package main import ( "crypto/aes" "crypto/cipher" "encoding/hex" "fmt" "image/color" "os" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) const ( Version = "0.1.1" Title = "Secure Editor" AlgorithmAes256Gcm = "aes-256-gcm" // Exit codes: // 0 -> file was saved and the editor exited // 2 -> editor closed without saving ExitCodeOK = 0 ExitCodeNoSave = 2 ) func decrypt(ciphertext, key, nonce []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } aesgcm, err := cipher.NewGCM(block) if err != nil { return nil, err } plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) if err != nil { return nil, err } return plaintext, nil } func encrypt(plaintext, key, nonce []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } aesgcm, err := cipher.NewGCM(block) if err != nil { return nil, err } ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil) return ciphertext, nil } func writeFilePreserveMode(filename string, data []byte) error { fi, err := os.Stat(filename) if err != nil { if os.IsNotExist(err) { return os.WriteFile(filename, data, 0666) } return err } err = os.WriteFile(filename, data, 0666) if err != nil { return err } return os.Chmod(filename, fi.Mode()) } func showErrorMessage(err error, parent fyne.Window) { dialog.ShowError(err, parent) } // lightEditorTheme forces a light variant with a pure white input background // so the editable text area reads as a white editing surface regardless of // the system appearance setting. Readonly mode keeps the default theme (grey). type lightEditorTheme struct { fyne.Theme } func (t *lightEditorTheme) Color(name fyne.ThemeColorName, _ fyne.ThemeVariant) color.Color { if name == theme.ColorNameInputBackground { return color.White } // Always resolve against the light variant so white input background // stays paired with dark, readable text. return t.Theme.Color(name, theme.VariantLight) } func main() { args := os.Args if len(args) == 2 && args[1] == "version" { fmt.Printf("secure-editor-go version: %s\n", Version) os.Exit(0) } if len(args) != 5 { fmt.Printf("Bad encrypt args: %v\n", args) os.Exit(1) } fileName := args[1] algorithm := args[2] if algorithm != AlgorithmAes256Gcm { fmt.Printf("Bad algorithm: %s\n", algorithm) os.Exit(1) } keyBytes, err := hex.DecodeString(args[3]) if err != nil { fmt.Printf("Bad key: %s, failed: %s\n", args[3], err.Error()) os.Exit(1) } if len(keyBytes) != 32 { fmt.Printf("Bad key length: %d\n", len(keyBytes)) os.Exit(1) } nonceBytes, err := hex.DecodeString(args[4]) if err != nil { fmt.Printf("Bad nonce: %s, failed: %s\n", args[4], err.Error()) os.Exit(1) } if len(nonceBytes) != 12 { fmt.Printf("Bad nonce length: %d\n", len(nonceBytes)) os.Exit(1) } fileData, err := os.ReadFile(fileName) if err != nil { fmt.Printf("Read file: %s, failed: %s\n", fileName, err.Error()) os.Exit(1) } plaintext, err := decrypt(fileData, keyBytes, nonceBytes) if err != nil { fmt.Printf("Decrypt file: %s, failed: %s\n", fileName, err.Error()) os.Exit(1) } isReadonly := isReadonly() secureEditorApp := app.New() secureEditorApp.SetIcon(appIcon()) if !isReadonly { secureEditorApp.Settings().SetTheme(&lightEditorTheme{Theme: theme.DefaultTheme()}) } window := secureEditorApp.NewWindow(Title) window.SetIcon(appIcon()) cfg := loadConfig() window.Resize(clampToScreen(fyne.NewSize(cfg.Width, cfg.Height))) // exitCode defaults to "closed without saving". The Save button raises // it to ExitCodeOK. It is read after the event loop ends. exitCode := ExitCodeNoSave window.SetOnClosed(func() { saveConfig(window.Canvas().Size()) secureEditorApp.Quit() }) textEntry := widget.NewMultiLineEntry() textEntry.Wrapping = fyne.TextWrapWord if fixedPitchEnabled() { // Use Fyne's bundled monospace font for a programmer-style editor. textEntry.TextStyle = fyne.TextStyle{Monospace: true} } initialText := string(plaintext) textEntry.SetText(initialText) if isReadonly { // textEntry.Disable() // TODO for font is grey textEntry.OnChanged = func(text string) { if text != initialText { textEntry.SetText(initialText) } } } exitButton := widget.NewButton(" [ Exit ] ", func() { exitCode = ExitCodeNoSave secureEditorApp.Quit() }) saveButton := widget.NewButton(" [ Save ] ", func() { plaintext := textEntry.Text ciphertext, err := encrypt([]byte(plaintext), keyBytes, nonceBytes) if err != nil { showErrorMessage(err, window) return } err = writeFilePreserveMode(fileName, ciphertext) if err != nil { showErrorMessage(err, window) return } exitCode = ExitCodeOK secureEditorApp.Quit() }) buttons := container.NewHBox(exitButton, saveButton) if isReadonly { buttons = container.NewHBox(exitButton) } content := container.NewBorder( nil, // top container.NewBorder(nil, nil, nil, buttons, nil), // bottom nil, // left nil, // right textEntry, // center ) window.SetContent(content) window.CenterOnScreen() window.ShowAndRun() os.Exit(exitCode) }