feature/ui - zatim ne uplne uhlazena ale celkem pouzitelna appka #1
79
fyne_ui.go
79
fyne_ui.go
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
encrypt "fckeuspy-go/lib"
|
encrypt "fckeuspy-go/lib"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -42,11 +43,20 @@ func NewUI() (stprageDir string, window fyne.Window) {
|
|||||||
|
|
||||||
// ShowPasswordVaultDialog zobrazí dialog pro vytvoření nebo otevření trezoru.
|
// ShowPasswordVaultDialog zobrazí dialog pro vytvoření nebo otevření trezoru.
|
||||||
// Zatím pouze skeleton: vrací heslo přes callback, reálné volání CreateEncryptedStore/OpenEncryptedStore mimo.
|
// Zatím pouze skeleton: vrací heslo přes callback, reálné volání CreateEncryptedStore/OpenEncryptedStore mimo.
|
||||||
func ShowPasswordVaultDialog(w fyne.Window, onResult func(create bool, password string)) {
|
func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(create bool, password string)) {
|
||||||
pwEntry := widget.NewPasswordEntry()
|
// Unlock tab
|
||||||
pwEntry.SetPlaceHolder("Heslo…")
|
unlockPw := widget.NewPasswordEntry()
|
||||||
pw2Entry := widget.NewPasswordEntry()
|
unlockPw.SetPlaceHolder("Heslo…")
|
||||||
pw2Entry.SetPlaceHolder("Znovu heslo…")
|
unlockForm := widget.NewForm(
|
||||||
|
widget.NewFormItem("Heslo", unlockPw),
|
||||||
|
)
|
||||||
|
unlockContent := container.NewVBox(unlockForm)
|
||||||
|
|
||||||
|
// Create tab
|
||||||
|
createPw1 := widget.NewPasswordEntry()
|
||||||
|
createPw1.SetPlaceHolder("Heslo…")
|
||||||
|
createPw2 := widget.NewPasswordEntry()
|
||||||
|
createPw2.SetPlaceHolder("Znovu heslo…")
|
||||||
strengthBar := widget.NewProgressBar()
|
strengthBar := widget.NewProgressBar()
|
||||||
strengthBar.Min = 0
|
strengthBar.Min = 0
|
||||||
strengthBar.Max = 100
|
strengthBar.Max = 100
|
||||||
@ -56,41 +66,43 @@ func ShowPasswordVaultDialog(w fyne.Window, onResult func(create bool, password
|
|||||||
strengthBar.SetValue(float64(score))
|
strengthBar.SetValue(float64(score))
|
||||||
strengthLabel.SetText(desc)
|
strengthLabel.SetText(desc)
|
||||||
}
|
}
|
||||||
pwEntry.OnChanged = updateStrength
|
createPw1.OnChanged = updateStrength
|
||||||
genBtn := widget.NewButton("Generovat", func() {
|
genBtn := widget.NewButton("Generovat", func() {
|
||||||
if v, err := encrypt.GenerateRandomPassword(20); err == nil {
|
if v, err := encrypt.GenerateRandomPassword(20); err == nil {
|
||||||
pwEntry.SetText(v)
|
createPw1.SetText(v)
|
||||||
pw2Entry.SetText(v)
|
createPw2.SetText(v)
|
||||||
updateStrength(v)
|
updateStrength(v)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
modeCreate := true
|
|
||||||
var toggle *widget.Button
|
|
||||||
toggle = widget.NewButton("Režim: Vytvořit (klikni pro Otevřít)", func() {
|
|
||||||
modeCreate = !modeCreate
|
|
||||||
if modeCreate {
|
|
||||||
toggle.SetText("Režim: Vytvořit (klikni pro Otevřít)")
|
|
||||||
pw2Entry.Enable()
|
|
||||||
} else {
|
|
||||||
toggle.SetText("Režim: Otevřít (klikni pro Vytvořit)")
|
|
||||||
pw2Entry.Disable()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
form := &widget.Form{Items: []*widget.FormItem{
|
|
||||||
{Text: "Heslo", Widget: pwEntry},
|
|
||||||
{Text: "Potvrzení", Widget: pw2Entry},
|
|
||||||
}}
|
|
||||||
meter := container.NewVBox(strengthBar, strengthLabel)
|
meter := container.NewVBox(strengthBar, strengthLabel)
|
||||||
topRow := container.NewBorder(nil, nil, nil, genBtn, meter)
|
topCreate := container.NewBorder(nil, nil, nil, genBtn, meter)
|
||||||
content := container.NewVBox(toggle, topRow, form)
|
createForm := widget.NewForm(
|
||||||
d := dialog.NewCustomConfirm("Trezor", "OK", "Zrušit", content, func(ok bool) {
|
widget.NewFormItem("Heslo", createPw1),
|
||||||
|
widget.NewFormItem("Potvrzení", createPw2),
|
||||||
|
)
|
||||||
|
createContent := container.NewVBox(topCreate, createForm)
|
||||||
|
|
||||||
|
tabs := container.NewAppTabs(
|
||||||
|
container.NewTabItem("Odemknout", unlockContent),
|
||||||
|
container.NewTabItem("Vytvořit", createContent),
|
||||||
|
)
|
||||||
|
tabs.SetTabLocation(container.TabLocationTop)
|
||||||
|
// Výběr aktivní záložky dle existence souboru
|
||||||
|
if _, err := os.Stat(vaultPath); err != nil { // neexistuje => vytvořit
|
||||||
|
tabs.SelectIndex(1)
|
||||||
|
} else {
|
||||||
|
tabs.SelectIndex(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
d := dialog.NewCustomConfirm("Trezor", "OK", "Zrušit", tabs, func(ok bool) {
|
||||||
if !ok {
|
if !ok {
|
||||||
onResult(false, "")
|
onResult(false, "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pw := pwEntry.Text
|
sel := tabs.Selected()
|
||||||
if modeCreate {
|
if sel != nil && sel.Text == "Vytvořit" {
|
||||||
if pw != pw2Entry.Text {
|
pw := createPw1.Text
|
||||||
|
if pw != createPw2.Text {
|
||||||
dialog.NewError(errors.New("Hesla se neshodují"), w).Show()
|
dialog.NewError(errors.New("Hesla se neshodují"), w).Show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -98,9 +110,14 @@ func ShowPasswordVaultDialog(w fyne.Window, onResult func(create bool, password
|
|||||||
dialog.NewError(err, w).Show()
|
dialog.NewError(err, w).Show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
onResult(true, pw)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
onResult(modeCreate, pw)
|
// Unlock
|
||||||
|
onResult(false, unlockPw.Text)
|
||||||
}, w)
|
}, w)
|
||||||
|
// Větší rozměr pro lepší přehlednost
|
||||||
|
d.Resize(fyne.NewSize(620, 440))
|
||||||
d.Show()
|
d.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
103
lib/crypto.go
103
lib/crypto.go
@ -32,7 +32,6 @@ type Service struct {
|
|||||||
PubPEM []byte `json:"pub,omitempty"`
|
PubPEM []byte `json:"pub,omitempty"`
|
||||||
CertPEM []byte `json:"cert,omitempty"`
|
CertPEM []byte `json:"cert,omitempty"`
|
||||||
dir string `json:"-"`
|
dir string `json:"-"`
|
||||||
symKey []byte `json:"-"` // interní AES klíč pro lokální šifrování
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(storageDir string) (*Service, error) {
|
func NewService(storageDir string) (*Service, error) {
|
||||||
@ -166,7 +165,6 @@ func (s *Service) loadOrGenerateKeys() error {
|
|||||||
privPath := filepath.Join(s.dir, "identity_key.pem")
|
privPath := filepath.Join(s.dir, "identity_key.pem")
|
||||||
pubPath := filepath.Join(s.dir, "public.pem")
|
pubPath := filepath.Join(s.dir, "public.pem")
|
||||||
certPath := filepath.Join(s.dir, "identity.crt")
|
certPath := filepath.Join(s.dir, "identity.crt")
|
||||||
symPath := filepath.Join(s.dir, "sym.key") // base64(klic)
|
|
||||||
|
|
||||||
fmt.Printf("Using storage dir: %s\n", s.dir)
|
fmt.Printf("Using storage dir: %s\n", s.dir)
|
||||||
if fileExists(privPath) && fileExists(pubPath) && fileExists(certPath) {
|
if fileExists(privPath) && fileExists(pubPath) && fileExists(certPath) {
|
||||||
@ -185,25 +183,6 @@ func (s *Service) loadOrGenerateKeys() error {
|
|||||||
s.Priv = key
|
s.Priv = key
|
||||||
s.PubPEM, _ = os.ReadFile(pubPath)
|
s.PubPEM, _ = os.ReadFile(pubPath)
|
||||||
s.CertPEM, _ = os.ReadFile(certPath)
|
s.CertPEM, _ = os.ReadFile(certPath)
|
||||||
// Načti symetrický klíč (pokud existuje); pokud ne, vytvoř.
|
|
||||||
if fileExists(symPath) {
|
|
||||||
b, err := os.ReadFile(symPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
decoded, err := base64.StdEncoding.DecodeString(string(b))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("sym.key base64 decode: %w", err)
|
|
||||||
}
|
|
||||||
if l := len(decoded); l != 16 && l != 24 && l != 32 {
|
|
||||||
return fmt.Errorf("unsupported sym key length: %d", l)
|
|
||||||
}
|
|
||||||
s.symKey = decoded
|
|
||||||
} else {
|
|
||||||
if err := s.generateAndPersistSymKey(symPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,91 +213,9 @@ func (s *Service) loadOrGenerateKeys() error {
|
|||||||
if err := os.WriteFile(certPath, s.CertPEM, 0o644); err != nil {
|
if err := os.WriteFile(certPath, s.CertPEM, 0o644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Vytvoř symetrický klíč
|
|
||||||
if err := s.generateAndPersistSymKey(symPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateAndPersistSymKey vytvoří nový AES-256 klíč a uloží jej (pokud existuje dir).
|
|
||||||
func (s *Service) generateAndPersistSymKey(path string) error {
|
|
||||||
k := make([]byte, 32)
|
|
||||||
if _, err := rand.Read(k); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.symKey = k
|
|
||||||
if s.dir == "" { // ephemeral (např. web mód bez persistence)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
enc := base64.StdEncoding.EncodeToString(k)
|
|
||||||
return os.WriteFile(path, []byte(enc), 0o600)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SymEnvelope je malý formát pro interní symetrické šifrování.
|
|
||||||
type SymEnvelope struct {
|
|
||||||
N string `json:"n"` // base64(nonce)
|
|
||||||
CT string `json:"ct"` // base64(ciphertext||tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SymmetricEncrypt šifruje plaintext pomocí interního AES-GCM klíče.
|
|
||||||
func (s *Service) SymmetricEncrypt(plain string) (string, error) {
|
|
||||||
if len(s.symKey) == 0 {
|
|
||||||
return "", errors.New("sym key not initialized")
|
|
||||||
}
|
|
||||||
block, err := aes.NewCipher(s.symKey)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
ct := gcm.Seal(nil, nonce, []byte(plain), nil)
|
|
||||||
env := SymEnvelope{
|
|
||||||
N: base64.StdEncoding.EncodeToString(nonce),
|
|
||||||
CT: base64.StdEncoding.EncodeToString(ct),
|
|
||||||
}
|
|
||||||
out, _ := json.Marshal(env)
|
|
||||||
return string(out), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SymmetricDecrypt dešifruje JSON envelope vytvořený SymmetricEncrypt.
|
|
||||||
func (s *Service) SymmetricDecrypt(payload string) (string, error) {
|
|
||||||
if len(s.symKey) == 0 {
|
|
||||||
return "", errors.New("sym key not initialized")
|
|
||||||
}
|
|
||||||
var env SymEnvelope
|
|
||||||
if err := json.Unmarshal([]byte(payload), &env); err != nil {
|
|
||||||
return "", fmt.Errorf("invalid sym envelope json: %w", err)
|
|
||||||
}
|
|
||||||
nonce, err := base64.StdEncoding.DecodeString(env.N)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("nonce b64: %w", err)
|
|
||||||
}
|
|
||||||
ct, err := base64.StdEncoding.DecodeString(env.CT)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("ct b64: %w", err)
|
|
||||||
}
|
|
||||||
block, err := aes.NewCipher(s.symKey)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
gcm, err := cipher.NewGCM(block)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
pt, err := gcm.Open(nil, nonce, ct, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("gcm open: %w", err)
|
|
||||||
}
|
|
||||||
return string(pt), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateSelfSignedCert(priv *rsa.PrivateKey) ([]byte, error) {
|
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()),
|
||||||
|
|||||||
7
main.go
7
main.go
@ -45,8 +45,9 @@ func runFyne() {
|
|||||||
w.SetContent(placeholder)
|
w.SetContent(placeholder)
|
||||||
|
|
||||||
showDialog := func() {
|
showDialog := func() {
|
||||||
ShowPasswordVaultDialog(w, func(create bool, password string) {
|
ShowPasswordVaultDialog(w, vaultPath, func(create bool, password string) {
|
||||||
if password == "" {
|
if password == "" { // Cancel nebo zavření dialogu => ukonči app
|
||||||
|
fyne.CurrentApp().Quit()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var store encrypt.SecureJSONStore
|
var store encrypt.SecureJSONStore
|
||||||
@ -71,7 +72,7 @@ func runFyne() {
|
|||||||
}
|
}
|
||||||
parts := buildEntries()
|
parts := buildEntries()
|
||||||
fyne.CurrentApp().Driver().AllWindows()[0].SetTitle("Encryptor (Vault)")
|
fyne.CurrentApp().Driver().AllWindows()[0].SetTitle("Encryptor (Vault)")
|
||||||
w.SetContent(buildTabbedUI(parts, vs))
|
w.SetContent(buildTabbedUI(parts, vs, vaultPath))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
39
ui.go
39
ui.go
@ -1,12 +1,16 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
encrypt "fckeuspy-go/lib"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/canvas"
|
"fyne.io/fyne/v2/canvas"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/dialog"
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
)
|
)
|
||||||
@ -80,7 +84,7 @@ func (simpleTheme) Size(n fyne.ThemeSizeName) float32 { return theme.Defau
|
|||||||
var forceDark = true
|
var forceDark = true
|
||||||
|
|
||||||
// Build key section
|
// Build key section
|
||||||
func buildIdentityTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
|
||||||
btnCopyPub := widget.NewButton("Copy public.pem", func() { copyClip(svc.PublicPEM(), parts) })
|
btnCopyPub := widget.NewButton("Copy public.pem", func() { copyClip(svc.PublicPEM(), parts) })
|
||||||
btnCopyCrt := widget.NewButton("Copy identity.crt", func() { copyClip(svc.PublicCert(), parts) })
|
btnCopyCrt := widget.NewButton("Copy identity.crt", func() { copyClip(svc.PublicCert(), parts) })
|
||||||
btnShowPub := widget.NewButton("Show pub", func() { parts.outKey.SetText(svc.PublicPEM()) })
|
btnShowPub := widget.NewButton("Show pub", func() { parts.outKey.SetText(svc.PublicPEM()) })
|
||||||
@ -88,7 +92,34 @@ func buildIdentityTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
btnClear := widget.NewButton("Clear", func() { parts.outKey.SetText("") })
|
btnClear := widget.NewButton("Clear", func() { parts.outKey.SetText("") })
|
||||||
btnPaste := widget.NewButton("Paste", func() { parts.outKey.SetText(fyne.CurrentApp().Clipboard().Content()) })
|
btnPaste := widget.NewButton("Paste", func() { parts.outKey.SetText(fyne.CurrentApp().Clipboard().Content()) })
|
||||||
|
|
||||||
tileIdentity := buttonTile(btnCopyPub, btnCopyCrt, btnPaste, btnShowPub, btnShowCrt, btnClear)
|
deleteBtn := widget.NewButton("Smazat identitu", func() {
|
||||||
|
// dialog pro heslo
|
||||||
|
pwEntry := widget.NewPasswordEntry()
|
||||||
|
pwEntry.SetPlaceHolder("Heslo pro potvrzení…")
|
||||||
|
content := widget.NewForm(widget.NewFormItem("Heslo", pwEntry))
|
||||||
|
dialog.ShowCustomConfirm("Potvrdit smazání", "Smazat", "Zrušit", content, func(ok bool) {
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pw := pwEntry.Text
|
||||||
|
if pw == "" {
|
||||||
|
dialog.NewError(errors.New("heslo je prázdné"), fyne.CurrentApp().Driver().AllWindows()[0]).Show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := encrypt.OpenEncryptedStore(vaultPath, pw); err != nil {
|
||||||
|
dialog.NewError(errors.New("neplatné heslo"), fyne.CurrentApp().Driver().AllWindows()[0]).Show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := os.Remove(vaultPath); err != nil {
|
||||||
|
dialog.NewError(err, fyne.CurrentApp().Driver().AllWindows()[0]).Show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Quit app po odstranění (uživatel musí znovu spustit a vytvořit nový vault)
|
||||||
|
fyne.CurrentApp().Quit()
|
||||||
|
}, fyne.CurrentApp().Driver().AllWindows()[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
tileIdentity := buttonTile(btnCopyPub, btnCopyCrt, btnPaste, btnShowPub, btnShowCrt, btnClear, 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,
|
tileIdentity,
|
||||||
@ -189,8 +220,8 @@ func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
return container.NewVScroll(group)
|
return container.NewVScroll(group)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTabbedUI(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
|
||||||
idTab := container.NewTabItem("Identita", buildIdentityTab(parts, svc))
|
idTab := container.NewTabItem("Identita", buildIdentityTab(parts, svc, vaultPath))
|
||||||
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, encTab, decTab)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user