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 (
|
||||
"errors"
|
||||
encrypt "fckeuspy-go/lib"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@ -42,11 +43,20 @@ func NewUI() (stprageDir string, window fyne.Window) {
|
||||
|
||||
// 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.
|
||||
func ShowPasswordVaultDialog(w fyne.Window, onResult func(create bool, password string)) {
|
||||
pwEntry := widget.NewPasswordEntry()
|
||||
pwEntry.SetPlaceHolder("Heslo…")
|
||||
pw2Entry := widget.NewPasswordEntry()
|
||||
pw2Entry.SetPlaceHolder("Znovu heslo…")
|
||||
func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(create bool, password string)) {
|
||||
// Unlock tab
|
||||
unlockPw := widget.NewPasswordEntry()
|
||||
unlockPw.SetPlaceHolder("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.Min = 0
|
||||
strengthBar.Max = 100
|
||||
@ -56,41 +66,43 @@ func ShowPasswordVaultDialog(w fyne.Window, onResult func(create bool, password
|
||||
strengthBar.SetValue(float64(score))
|
||||
strengthLabel.SetText(desc)
|
||||
}
|
||||
pwEntry.OnChanged = updateStrength
|
||||
createPw1.OnChanged = updateStrength
|
||||
genBtn := widget.NewButton("Generovat", func() {
|
||||
if v, err := encrypt.GenerateRandomPassword(20); err == nil {
|
||||
pwEntry.SetText(v)
|
||||
pw2Entry.SetText(v)
|
||||
createPw1.SetText(v)
|
||||
createPw2.SetText(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)
|
||||
topRow := container.NewBorder(nil, nil, nil, genBtn, meter)
|
||||
content := container.NewVBox(toggle, topRow, form)
|
||||
d := dialog.NewCustomConfirm("Trezor", "OK", "Zrušit", content, func(ok bool) {
|
||||
topCreate := container.NewBorder(nil, nil, nil, genBtn, meter)
|
||||
createForm := widget.NewForm(
|
||||
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 {
|
||||
onResult(false, "")
|
||||
return
|
||||
}
|
||||
pw := pwEntry.Text
|
||||
if modeCreate {
|
||||
if pw != pw2Entry.Text {
|
||||
sel := tabs.Selected()
|
||||
if sel != nil && sel.Text == "Vytvořit" {
|
||||
pw := createPw1.Text
|
||||
if pw != createPw2.Text {
|
||||
dialog.NewError(errors.New("Hesla se neshodují"), w).Show()
|
||||
return
|
||||
}
|
||||
@ -98,9 +110,14 @@ func ShowPasswordVaultDialog(w fyne.Window, onResult func(create bool, password
|
||||
dialog.NewError(err, w).Show()
|
||||
return
|
||||
}
|
||||
onResult(true, pw)
|
||||
return
|
||||
}
|
||||
onResult(modeCreate, pw)
|
||||
// Unlock
|
||||
onResult(false, unlockPw.Text)
|
||||
}, w)
|
||||
// Větší rozměr pro lepší přehlednost
|
||||
d.Resize(fyne.NewSize(620, 440))
|
||||
d.Show()
|
||||
}
|
||||
|
||||
|
||||
103
lib/crypto.go
103
lib/crypto.go
@ -32,7 +32,6 @@ type Service struct {
|
||||
PubPEM []byte `json:"pub,omitempty"`
|
||||
CertPEM []byte `json:"cert,omitempty"`
|
||||
dir string `json:"-"`
|
||||
symKey []byte `json:"-"` // interní AES klíč pro lokální šifrování
|
||||
}
|
||||
|
||||
func NewService(storageDir string) (*Service, error) {
|
||||
@ -166,7 +165,6 @@ 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")
|
||||
symPath := filepath.Join(s.dir, "sym.key") // base64(klic)
|
||||
|
||||
fmt.Printf("Using storage dir: %s\n", s.dir)
|
||||
if fileExists(privPath) && fileExists(pubPath) && fileExists(certPath) {
|
||||
@ -185,25 +183,6 @@ func (s *Service) loadOrGenerateKeys() error {
|
||||
s.Priv = key
|
||||
s.PubPEM, _ = os.ReadFile(pubPath)
|
||||
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
|
||||
}
|
||||
|
||||
@ -234,91 +213,9 @@ func (s *Service) loadOrGenerateKeys() error {
|
||||
if err := os.WriteFile(certPath, s.CertPEM, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
// Vytvoř symetrický klíč
|
||||
if err := s.generateAndPersistSymKey(symPath); err != nil {
|
||||
return err
|
||||
}
|
||||
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) {
|
||||
tpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||
|
||||
7
main.go
7
main.go
@ -45,8 +45,9 @@ func runFyne() {
|
||||
w.SetContent(placeholder)
|
||||
|
||||
showDialog := func() {
|
||||
ShowPasswordVaultDialog(w, func(create bool, password string) {
|
||||
if password == "" {
|
||||
ShowPasswordVaultDialog(w, vaultPath, func(create bool, password string) {
|
||||
if password == "" { // Cancel nebo zavření dialogu => ukonči app
|
||||
fyne.CurrentApp().Quit()
|
||||
return
|
||||
}
|
||||
var store encrypt.SecureJSONStore
|
||||
@ -71,7 +72,7 @@ func runFyne() {
|
||||
}
|
||||
parts := buildEntries()
|
||||
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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
encrypt "fckeuspy-go/lib"
|
||||
"image/color"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
@ -80,7 +84,7 @@ func (simpleTheme) Size(n fyne.ThemeSizeName) float32 { return theme.Defau
|
||||
var forceDark = true
|
||||
|
||||
// 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) })
|
||||
btnCopyCrt := widget.NewButton("Copy identity.crt", func() { copyClip(svc.PublicCert(), parts) })
|
||||
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("") })
|
||||
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(
|
||||
widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
tileIdentity,
|
||||
@ -189,8 +220,8 @@ func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
||||
return container.NewVScroll(group)
|
||||
}
|
||||
|
||||
func buildTabbedUI(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
||||
idTab := container.NewTabItem("Identita", buildIdentityTab(parts, svc))
|
||||
func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
|
||||
idTab := container.NewTabItem("Identita", buildIdentityTab(parts, svc, vaultPath))
|
||||
encTab := container.NewTabItem("Šifrování", buildEncryptTab(parts, svc))
|
||||
decTab := container.NewTabItem("Dešifrování", buildDecryptTab(parts, svc))
|
||||
tabs := container.NewAppTabs(idTab, encTab, decTab)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user