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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Envelope struct {
|
// No envelope here; server/UI define their own payload structs.
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePeerPublicKey(pemOrCert string) (*rsa.PublicKey, error) {
|
func parsePeerPublicKey(pemOrCert string) (*rsa.PublicKey, error) {
|
||||||
block, _ := pem.Decode([]byte(pemOrCert))
|
block, _ := pem.Decode([]byte(pemOrCert))
|
||||||
@ -161,65 +52,13 @@ func ParsePeerPublicKey(pemOrCert string) (*rsa.PublicKey, error) {
|
|||||||
return parsePeerPublicKey(pemOrCert)
|
return parsePeerPublicKey(pemOrCert)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) loadOrGenerateKeys() error {
|
func generateSelfSignedCert(priv *rsa.PrivateKey, commonName string) ([]byte, error) {
|
||||||
privPath := filepath.Join(s.dir, "identity_key.pem")
|
if commonName == "" {
|
||||||
pubPath := filepath.Join(s.dir, "public.pem")
|
commonName = "Encryptor Local Identity"
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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{
|
tpl := &x509.Certificate{
|
||||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
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),
|
NotBefore: time.Now().Add(-1 * time.Hour),
|
||||||
NotAfter: time.Now().AddDate(1, 0, 0),
|
NotAfter: time.Now().AddDate(1, 0, 0),
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
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})
|
_ = pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||||
return buf.Bytes(), nil
|
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)})
|
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 (krátký CN)
|
// Optional self-signed cert s rozumným CommonName (lze upravit později UI)
|
||||||
certPEM, _ := generateSelfSignedCert(pk)
|
certPEM, _ := generateSelfSignedCert(pk, "Local User")
|
||||||
_ = 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))
|
||||||
|
|||||||
41
main.go
41
main.go
@ -2,6 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
encrypt "fckeuspy-go/lib"
|
encrypt "fckeuspy-go/lib"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
@ -82,12 +84,45 @@ func runFyne() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RunWebApp() {
|
func RunWebApp() {
|
||||||
var err error
|
// Otevři nebo vytvoř šifrovaný trezor a načti identitu pouze z něj
|
||||||
// 2) inicializuj šifrovací službu
|
vaultPath := os.Getenv("VAULT_PATH")
|
||||||
_, err = encrypt.NewService("")
|
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 {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
priv = key
|
||||||
|
pubPEM = []byte(store.IdentityPublicPEM())
|
||||||
|
certPEM = []byte(store.IdentityCertPEM())
|
||||||
|
|
||||||
// 2) šablony
|
// 2) šablony
|
||||||
tmpl = template.Must(template.ParseGlob("templates/*.html"))
|
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,
|
showPayloadQR: true,
|
||||||
}
|
}
|
||||||
p.cipherQR.FillMode = canvas.ImageFillContain
|
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.FillMode = canvas.ImageFillContain
|
||||||
p.pubQR.SetMinSize(fyne.NewSize(220, 220))
|
p.pubQR.SetMinSize(fyne.NewSize(200, 200))
|
||||||
p.crtQR.FillMode = canvas.ImageFillContain
|
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.FillMode = canvas.ImageFillContain
|
||||||
p.peerQR.SetMinSize(fyne.NewSize(220, 220))
|
p.peerQR.SetMinSize(fyne.NewSize(200, 200))
|
||||||
p.payloadQR.FillMode = canvas.ImageFillContain
|
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.outKey.SetPlaceHolder("Veřejný klíč / certifikát…")
|
||||||
p.msg.SetPlaceHolder("Sem napiš zprávu…")
|
p.msg.SetPlaceHolder("Sem napiš zprávu…")
|
||||||
p.peer.SetPlaceHolder("-----BEGIN PUBLIC KEY----- … nebo CERTIFICATE …")
|
p.peer.SetPlaceHolder("-----BEGIN PUBLIC KEY----- … nebo CERTIFICATE …")
|
||||||
@ -70,12 +70,12 @@ func buildEntries() *uiParts {
|
|||||||
p.payload.SetPlaceHolder(`{"ek":"…","n":"…","ct":"…"}`)
|
p.payload.SetPlaceHolder(`{"ek":"…","n":"…","ct":"…"}`)
|
||||||
p.plainOut.SetPlaceHolder("Dešifrovaná zpráva…")
|
p.plainOut.SetPlaceHolder("Dešifrovaná zpráva…")
|
||||||
// Zvýšení výšky (více řádků viditelně)
|
// Zvýšení výšky (více řádků viditelně)
|
||||||
p.outKey.SetMinRowsVisible(10)
|
p.outKey.SetMinRowsVisible(6)
|
||||||
p.peer.SetMinRowsVisible(6)
|
p.peer.SetMinRowsVisible(4)
|
||||||
p.msg.SetMinRowsVisible(8)
|
p.msg.SetMinRowsVisible(5)
|
||||||
p.cipherOut.SetMinRowsVisible(8)
|
p.cipherOut.SetMinRowsVisible(5)
|
||||||
p.payload.SetMinRowsVisible(8)
|
p.payload.SetMinRowsVisible(5)
|
||||||
p.plainOut.SetMinRowsVisible(8)
|
p.plainOut.SetMinRowsVisible(5)
|
||||||
p.toastLabel.Hide()
|
p.toastLabel.Hide()
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
@ -201,11 +201,16 @@ func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.
|
|||||||
toggleBtn.OnTapped = func() { parts.showQR = !parts.showQR; rebuild() }
|
toggleBtn.OnTapped = func() { parts.showQR = !parts.showQR; rebuild() }
|
||||||
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(
|
group := container.NewVBox(
|
||||||
widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
tileIdentity,
|
viewRow,
|
||||||
identityContainer,
|
identityContainer,
|
||||||
|
clipboardRow,
|
||||||
|
destroyRow,
|
||||||
)
|
)
|
||||||
return container.NewVScroll(group)
|
return container.NewVScroll(group)
|
||||||
}
|
}
|
||||||
@ -218,6 +223,10 @@ type ServiceFacade interface {
|
|||||||
Decrypt(json string) (string, error)
|
Decrypt(json string) (string, error)
|
||||||
PublicPEM() string
|
PublicPEM() string
|
||||||
PublicCert() 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) {
|
func copyClip(s string, parts *uiParts) {
|
||||||
@ -367,13 +376,26 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
widget.NewButton("Import Key QR", importPeerQR),
|
widget.NewButton("Import Key QR", importPeerQR),
|
||||||
)
|
)
|
||||||
|
|
||||||
group := container.NewVBox(
|
peerSection := container.NewVBox(
|
||||||
widget.NewLabelWithStyle("Šifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
container.NewHBox(widget.NewLabel("Veřejný klíč příjemce"), peerToggle),
|
||||||
container.NewHBox(widget.NewLabel("Veřejný klíč příjemce"), peerToggle), peerBtns, peerContainer,
|
peerContainer,
|
||||||
widget.NewLabel("Zpráva"), msgBtns, parts.msg,
|
peerBtns,
|
||||||
|
)
|
||||||
|
msgSection := container.NewVBox(
|
||||||
|
widget.NewLabel("Zpráva"),
|
||||||
|
parts.msg,
|
||||||
|
msgBtns,
|
||||||
|
)
|
||||||
|
outputSection := container.NewVBox(
|
||||||
container.NewHBox(widget.NewLabel("Výstup"), toggleBtn),
|
container.NewHBox(widget.NewLabel("Výstup"), toggleBtn),
|
||||||
outputContainer,
|
outputContainer,
|
||||||
)
|
)
|
||||||
|
group := container.NewVBox(
|
||||||
|
widget.NewLabelWithStyle("Šifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
|
peerSection,
|
||||||
|
msgSection,
|
||||||
|
outputSection,
|
||||||
|
)
|
||||||
return container.NewVScroll(group)
|
return container.NewVScroll(group)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -480,19 +502,30 @@ func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
}
|
}
|
||||||
updateMode()
|
updateMode()
|
||||||
plainBtns := buttonTile(widget.NewButton("Copy", func() { copyClip(parts.plainOut.Text, parts) }))
|
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(
|
group := container.NewVBox(
|
||||||
widget.NewLabelWithStyle("Dešifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
widget.NewLabelWithStyle("Dešifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
container.NewHBox(widget.NewLabel("Payload"), payloadToggle), payloadBtns, payloadContainer,
|
payloadSection,
|
||||||
widget.NewLabel("Výsledek"), plainBtns, parts.plainOut,
|
resultSection,
|
||||||
)
|
)
|
||||||
return container.NewVScroll(group)
|
return container.NewVScroll(group)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
|
func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
|
||||||
idTab := container.NewTabItem("Identita", buildIdentityTab(parts, svc, vaultPath))
|
idTab := container.NewTabItem("Identita", buildIdentityTab(parts, svc, vaultPath))
|
||||||
|
contactsTab := container.NewTabItem("Kontakty", buildContactsTab(parts, svc))
|
||||||
encTab := container.NewTabItem("Šifrování", buildEncryptTab(parts, svc))
|
encTab := container.NewTabItem("Šifrování", buildEncryptTab(parts, svc))
|
||||||
decTab := container.NewTabItem("Dešifrování", buildDecryptTab(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)
|
tabs.SetTabLocation(container.TabLocationTop)
|
||||||
|
|
||||||
// apply fixed dark theme once
|
// apply fixed dark theme once
|
||||||
@ -532,3 +565,110 @@ func buttonTile(btns ...fyne.CanvasObject) fyne.CanvasObject {
|
|||||||
padded := container.NewPadded(grid)
|
padded := container.NewPadded(grid)
|
||||||
return container.NewStack(rect, padded)
|
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"`
|
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) {
|
func encryptHybrid(priv *rsa.PrivateKey, message, peerPEMorCert string) (string, error) {
|
||||||
pubKey, err := encrypt.ParsePeerPublicKey(peerPEMorCert)
|
pubKey, err := encrypt.ParsePeerPublicKey(peerPEMorCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user