一個用 Go 撰寫的本地端加密實作程式
作者:Pecezen
平台:Windows
語言:Go
在資訊安全領域中,「加密」是一個經常被提到的主題,當然市面上也很多產品,但總想自己試著做看看,從密鑰管理、演算法選擇,寫一支簡單可實際操作的加解密工具,最近剛好有點時間,放上來供大家參考。
✅ 工具功能簡介
- 📁 資料夾加密 / 解密
- 📄 單一檔案加密 / 解密
- 🔑 金鑰檔案模式
- 🔐 自訂密碼模式
- 🧠 可切換不同加密方式(如 AES / SecretBox)
- 🌐 多語系介面(繁中 / English / 日本語/德文)
- 📊 加解密進度顯示
🖥️ 操作介面

加密功能說明
🔑 金鑰與密碼設計
- 金鑰檔案模式
- 使用獨立的 key file(如
secret.key) - 適合模擬企業環境的金鑰管理概念
- 使用獨立的 key file(如
- 密碼模式
- 由使用者自行輸入
- 用於理解密碼衍生與風險
🔐 加密演算法
- AES
- 主流對稱式加密
- 常見於企業與標準中
- SecretBox(NaCl / libsodium 概念)
- 封裝加密與驗證
- 適合理解「加密 + 完整性保護」
💾 Windows 執行檔下載
你可以透過下面的原始碼編譯要執行的程式,這邊也提供已編譯完成的可執行Windows程式,另外加密後請妥善保留加密金鑰或是密碼,如果遺失,就無法打開了!!!
檔案下載位置
下載後為壓縮檔,解密密碼:pecezen.org
🔗原始碼(Source Code)
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"fmt"
"image/color"
"os"
"path/filepath"
"strings"
"sync"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"golang.org/x/crypto/nacl/secretbox"
)
const (
DefaultKeyFile = "secret.key"
AppID = "FolderEncryptorV2"
SettingsFileName = "settings.json"
)
/* =========================
i18n:語言 & 字串表
========================= */
type Lang string
const (
LangZH Lang = "zh"
LangEN Lang = "en"
LangJA Lang = "ja"
LangDE Lang = "de"
)
var currentLang = LangZH
var i18n = map[Lang]map[string]string{
LangZH: {
"windowTitle": "🔐 檔案/資料夾加解密 V1.0",
"aboutBtn": "ℹ 關於",
"aboutTitle": "關於本程式",
"aboutContent": "作者: Pecezen.org\n\n版權宣告:\n本軟體採開源授權,轉載請註明出處。\n\n免責聲明:\n本程式僅供練習及測試使用,作者不對任何數據遺失或後果負責。\n請務必妥善保管您的金鑰或密碼。",
"langLabel": "介面語言:",
"langZH": "繁體中文",
"langEN": "English",
"langJA": "日本語",
"langDE": "Deutsch",
"selectFolder": "📁 選擇資料夾",
"selectFile": "📄 選擇單一檔案",
"targetLabel": "目標路徑:",
"noTarget": "尚未選擇目標",
"keyPathLabel": "金鑰路徑:",
"passwordLabel": "密碼:",
"changeKey": "變更位置",
"lockKey": "鎖定位置",
"modeLabel": "模式:",
"algoLabel": "算法:",
"modeKeyFile": "使用金鑰檔案",
"modePassword": "使用自訂密碼",
"algoSecretBox": "SecretBox (快速)",
"algoAES": "AES (標準)",
"encryptBtn": "🔒 開始加密",
"decryptBtn": "🔓 開始解密",
"noticeTitle": "提醒",
"noticeSelectTarget": "請先選擇檔案或資料夾",
"noticeEnterPassword": "請輸入密碼",
"noticePasswordMode": "目前為密碼模式,無需選擇金鑰檔。",
"logNoFiles": "ℹ 沒有可處理的檔案。",
"logFailFmt": "❌ %s 失敗: %v",
"logDoneFmt": "✅ %s 完成",
"logAllDone": "✔ 全部處理完成!",
"errNotEncrypted": "非加密檔案",
"errFileTooShort": "檔案長度不足",
"errDecryptFailed": "解密失敗 (可能是密碼/金鑰錯誤)",
"errTargetNotFound": "錯誤:找不到指定的目標路徑,請重新選擇。",
"logEncryptFailedFmt": "❌ 加密失敗: %v",
"logEncryptDoneFmt": "✅ 加密完成: %s.enc",
"logDecryptFailedFmt": "❌ 解密失敗: %v",
"logDecryptDoneFmt": "✅ 解密完成: %s",
},
LangEN: {
"windowTitle": "🔐 File/Folder Encryptor V1.0",
"aboutBtn": "ℹ About",
"aboutTitle": "About this App",
"aboutContent": "Author: Pecezen.org\n\nCopyright:\nThis software is open-source. Please credit the author when sharing.\n\nDisclaimer:\nThis software is for practice/testing only. The author is not responsible for any data loss. \nPlease keep your keys and passwords safe.",
"langLabel": "Language:",
"langZH": "繁體中文",
"langEN": "English",
"langJA": "日本語",
"langDE": "Deutsch",
"selectFolder": "📁 Select Folder",
"selectFile": "📄 Select File",
"targetLabel": "Target Path:",
"noTarget": "No target selected",
"keyPathLabel": "Key Path:",
"passwordLabel": "Password:",
"changeKey": "Change",
"lockKey": "Lock",
"modeLabel": "Mode:",
"algoLabel": "Algo:",
"modeKeyFile": "Key File",
"modePassword": "Password",
"algoSecretBox": "SecretBox",
"algoAES": "AES",
"encryptBtn": "🔒 Encrypt",
"decryptBtn": "🔓 Decrypt",
"noticeTitle": "Notice",
"noticeSelectTarget": "Please select a file or folder first.",
"noticeEnterPassword": "Please enter a password.",
"noticePasswordMode": "Currently in password mode; no key file is needed.",
"logNoFiles": "ℹ No files to process.",
"logFailFmt": "❌ %s failed: %v",
"logDoneFmt": "✅ %s done",
"logAllDone": "✔ All tasks completed!",
"errNotEncrypted": "not an encrypted file",
"errFileTooShort": "file length too short",
"errDecryptFailed": "decryption failed",
"errTargetNotFound": "Error: Target path not found. Please select again.",
"logEncryptFailedFmt": "❌ encryption failed: %v",
"logEncryptDoneFmt": "✅ encryption done: %s.enc",
"logDecryptFailedFmt": "❌ decryption failed: %v",
"logDecryptDoneFmt": "✅ decryption done: %s",
},
LangJA: {
"windowTitle": "🔐 ファイル/フォルダの暗号化 V1.0",
"aboutBtn": "ℹ 情報",
"aboutTitle": "このプログラムについて",
"aboutContent": "著者: Pecezen.org\n\n著作権:\nこのソフトウェアはオープンソースです。轉載時は著者を明記してください。\n\n免責事項:\n本ソフトは学習目的のみ。データの損失等について著者は一切の責任を負いません。",
"langLabel": "表示言語:",
"langZH": "繁體中文",
"langEN": "English",
"langJA": "日本語",
"langDE": "Deutsch",
"selectFolder": "📁 フォルダを選択",
"selectFile": "📄 ファイルを選択",
"targetLabel": "対象パス:",
"noTarget": "未選択",
"keyPathLabel": "鍵パス:",
"passwordLabel": "パス:",
"changeKey": "変更",
"lockKey": "ロック",
"modeLabel": "モード:",
"algoLabel": "アルゴリズム:",
"modeKeyFile": "鍵ファイル",
"modePassword": "パスワード",
"algoSecretBox": "SecretBox",
"algoAES": "AES",
"encryptBtn": "🔒 暗号化",
"decryptBtn": "🔓 復号",
"noticeTitle": "通知",
"noticeSelectTarget": "先にファイルかフォルダを選択してください。",
"noticeEnterPassword": "パスワードを入力してください。",
"noticePasswordMode": "パスワードモードです。鍵ファイルは不要です。",
"logNoFiles": "ℹ 対象ファイルがありません。",
"logFailFmt": "❌ %s 失敗: %v",
"logDoneFmt": "✅ %s 完了",
"logAllDone": "✔ すべての處理が完了しました!",
"errNotEncrypted": "暗号化ファイルではありません",
"errFileTooShort": "ファイル長が不足しています",
"errDecryptFailed": "復號に失敗しました",
"errTargetNotFound": "エラー:対象パスが見つかりません。再選擇してください。",
"logEncryptFailedFmt": "❌ 暗號化失敗: %v",
"logEncryptDoneFmt": "✅ 暗號化完了: %s.enc",
"logDecryptFailedFmt": "❌ 復號失敗: %v",
"logDecryptDoneFmt": "✅ 復號完了: %s",
},
LangDE: {
"windowTitle": "🔐 Datei/Ordner-Verschlüsselung V1.0",
"aboutBtn": "ℹ Über",
"aboutTitle": "Über diese App",
"aboutContent": "Autor: Pecezen.org\n\nCopyright:\nDiese Software ist Open-Source. Bitte geben Sie den Autor an.\n\nHaftungsausschluss:\nDiese Software dient nur zu Übungs-/Testzwecken. Der Autor haftet nicht für Datenverlust.\nBitte bewahren Sie Ihre Schlüssel und Passwörter sicher auf.",
"langLabel": "Sprache:",
"langZH": "繁體中文",
"langEN": "English",
"langJA": "日本語",
"langDE": "Deutsch",
"selectFolder": "📁 Ordner wählen",
"selectFile": "📄 Datei wählen",
"targetLabel": "Zielpfad:",
"noTarget": "Kein Ziel ausgewählt",
"keyPathLabel": "Schlüsselpfad:",
"passwordLabel": "Passwort:",
"changeKey": "Ändern",
"lockKey": "Sperren",
"modeLabel": "Modus:",
"algoLabel": "Algo:",
"modeKeyFile": "Schlüsseldatei",
"modePassword": "Passwort",
"algoSecretBox": "SecretBox",
"algoAES": "AES",
"encryptBtn": "🔒 Verschlüsseln",
"decryptBtn": "🔓 Entschlüsseln",
"noticeTitle": "Hinweis",
"noticeSelectTarget": "Bitte wählen Sie zuerst eine Datei oder einen Ordner aus.",
"noticeEnterPassword": "Bitte geben Sie ein Passwort ein.",
"noticePasswordMode": "Aktuell im Passwort-Modus; keine Schlüsseldatei erforderlich.",
"logNoFiles": "ℹ Keine Dateien zu verarbeiten.",
"logFailFmt": "❌ %s fehlgeschlagen: %v",
"logDoneFmt": "✅ %s erledigt",
"logAllDone": "✔ Alle Aufgaben abgeschlossen!",
"errNotEncrypted": "keine verschlüsselte Datei",
"errFileTooShort": "Dateilänge zu kurz",
"errDecryptFailed": "Entschlüsselung fehlgeschlagen",
"errTargetNotFound": "Fehler: Zielpfad nicht gefunden. Bitte erneut wählen.",
"logEncryptFailedFmt": "❌ Verschlüsselung fehlgeschlagen: %v",
"logEncryptDoneFmt": "✅ Verschlüsselung abgeschlossen: %s.enc",
"logDecryptFailedFmt": "❌ Entschlüsselung fehlgeschlagen: %v",
"logDecryptDoneFmt": "✅ Entschlüsselung abgeschlossen: %s",
},
}
func tr(key string) string {
if s, ok := i18n[currentLang][key]; ok {
return s
}
if s, ok := i18n[LangEN][key]; ok {
return s
}
return key
}
/* =========================
設定檔與主題
========================= */
type Settings struct {
Lang string `json:"lang"`
}
func settingsFilePath() (string, error) {
cfgDir, err := os.UserConfigDir()
if err != nil {
return "", err
}
return filepath.Join(cfgDir, AppID, SettingsFileName), nil
}
func loadLanguageFromSettings() Lang {
p, err := settingsFilePath()
if err != nil {
return LangZH
}
data, err := os.ReadFile(p)
if err != nil {
return LangZH
}
var s Settings
if err := json.Unmarshal(data, &s); err != nil {
return LangZH
}
switch strings.ToLower(s.Lang) {
case "en":
return LangEN
case "ja":
return LangJA
case "de":
return LangDE
default:
return LangZH
}
}
func saveLanguageToSettings(lang Lang) {
p, err := settingsFilePath()
if err != nil {
return
}
_ = os.MkdirAll(filepath.Dir(p), 0o700)
s := Settings{Lang: string(lang)}
b, _ := json.MarshalIndent(s, "", " ")
_ = os.WriteFile(p, b, 0o600)
}
type darkTextTheme struct{ fyne.Theme }
func (t darkTextTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
if name == theme.ColorNameDisabled {
return color.NRGBA{R: 100, G: 100, B: 100, A: 255}
}
if name == theme.ColorNameForeground {
return color.NRGBA{R: 30, G: 30, B: 30, A: 255}
}
return theme.DefaultTheme().Color(name, variant)
}
func ui(f func()) {
drv := fyne.CurrentApp().Driver()
if drv != nil {
type caller interface{ CallOnMain(func()) }
if c, ok := interface{}(drv).(caller); ok {
c.CallOnMain(f)
return
}
}
f()
}
/* =========================
Crypto & File Logic
========================= */
func deriveKeyFromPassword(password string) *[32]byte {
hash := sha256.Sum256([]byte(password))
var key [32]byte
copy(key[:], hash[:])
return &key
}
func loadOrCreateKey(path string) *[32]byte {
var key [32]byte
if _, err := os.Stat(path); err == nil {
data, _ := os.ReadFile(path)
if len(data) == 32 {
copy(key[:], data)
}
} else {
_, _ = rand.Read(key[:])
_ = os.MkdirAll(filepath.Dir(path), 0o700)
_ = os.WriteFile(path, key[:], 0o600)
}
return &key
}
func resolveDefaultKeyPath(target string, hasTarget bool) string {
if !hasTarget || target == "" {
return DefaultKeyFile
}
fi, err := os.Stat(target)
dir := target
if err != nil || !fi.IsDir() {
dir = filepath.Dir(target)
}
return filepath.Join(dir, DefaultKeyFile)
}
func atomicWriteFile(path string, data []byte, perm os.FileMode) error {
dir := filepath.Dir(path)
tmpFile, err := os.CreateTemp(dir, "tmp_enc_*")
if err != nil {
return err
}
tmpPath := tmpFile.Name()
if _, err := tmpFile.Write(data); err != nil {
_ = tmpFile.Close()
_ = os.Remove(tmpPath)
return err
}
_ = tmpFile.Sync()
_ = tmpFile.Close()
_ = os.Chmod(tmpPath, perm)
if err := os.Rename(tmpPath, path); err != nil {
_ = os.Remove(tmpPath)
return err
}
return nil
}
func removeWithRetry(path string) error {
_ = os.Chmod(path, 0o666)
var lastErr error
for i := 0; i < 3; i++ {
if err := os.Remove(path); err == nil {
return nil
} else {
lastErr = err
time.Sleep(200 * time.Millisecond)
}
}
return lastErr
}
func encryptFileSecretbox(filePath string, key *[32]byte) error {
data, err := os.ReadFile(filePath)
if err != nil {
return err
}
var nonce [24]byte
_, _ = rand.Read(nonce[:])
enc := secretbox.Seal(nonce[:], data, &nonce, key)
if err := atomicWriteFile(filePath+".enc", enc, 0o644); err != nil {
return err
}
return removeWithRetry(filePath)
}
func decryptFileSecretbox(encFilePath string, key *[32]byte) error {
if !strings.HasSuffix(encFilePath, ".enc") {
return fmt.Errorf(tr("errNotEncrypted"))
}
data, err := os.ReadFile(encFilePath)
if err != nil {
return err
}
if len(data) < 24 {
return fmt.Errorf(tr("errFileTooShort"))
}
var nonce [24]byte
copy(nonce[:], data[:24])
decrypted, ok := secretbox.Open(nil, data[24:], &nonce, key)
if !ok {
return fmt.Errorf(tr("errDecryptFailed"))
}
if err := atomicWriteFile(strings.TrimSuffix(encFilePath, ".enc"), decrypted, 0o644); err != nil {
return err
}
return removeWithRetry(encFilePath)
}
func encryptFileAES(filePath string, key *[32]byte) error {
data, err := os.ReadFile(filePath)
if err != nil {
return err
}
block, _ := aes.NewCipher(key[:])
gcm, _ := cipher.NewGCM(block)
nonce := make([]byte, gcm.NonceSize())
_, _ = rand.Read(nonce)
ciphertext := gcm.Seal(nonce, nonce, data, nil)
if err := atomicWriteFile(filePath+".enc", ciphertext, 0o644); err != nil {
return err
}
return removeWithRetry(filePath)
}
func decryptFileAES(encFilePath string, key *[32]byte) error {
if !strings.HasSuffix(encFilePath, ".enc") {
return fmt.Errorf(tr("errNotEncrypted"))
}
data, err := os.ReadFile(encFilePath)
if err != nil {
return err
}
block, _ := aes.NewCipher(key[:])
gcm, _ := cipher.NewGCM(block)
nz := gcm.NonceSize()
if len(data) < nz {
return fmt.Errorf(tr("errFileTooShort"))
}
nonce, ciphertext := data[:nz], data[nz:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return fmt.Errorf(tr("errDecryptFailed"))
}
if err := atomicWriteFile(strings.TrimSuffix(encFilePath, ".enc"), plaintext, 0o644); err != nil {
return err
}
return removeWithRetry(encFilePath)
}
func processFolder(folderPath string, key *[32]byte, action string, useAES bool, progress *widget.ProgressBar, log *widget.Entry) {
var files []string
_ = filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error {
if err == nil && !info.IsDir() {
if action == "encrypt" && !strings.HasSuffix(path, ".enc") && filepath.Base(path) != DefaultKeyFile {
files = append(files, path)
} else if action == "decrypt" && strings.HasSuffix(path, ".enc") {
files = append(files, path)
}
}
return nil
})
total := float64(len(files))
if total == 0 {
ui(func() { log.SetText(log.Text + tr("logNoFiles") + "\n"); progress.SetValue(1) })
return
}
var wg sync.WaitGroup
for i, f := range files {
wg.Add(1)
go func(idx int, path string) {
defer wg.Done()
var err error
if action == "encrypt" {
if useAES {
err = encryptFileAES(path, key)
} else {
err = encryptFileSecretbox(path, key)
}
} else {
if useAES {
err = decryptFileAES(path, key)
} else {
err = decryptFileSecretbox(path, key)
}
}
ui(func() {
if err != nil {
log.SetText(log.Text + fmt.Sprintf(tr("logFailFmt")+"\n", path, err))
} else {
log.SetText(log.Text + fmt.Sprintf(tr("logDoneFmt")+"\n", path))
}
progress.SetValue(float64(idx+1) / total)
})
}(i, f)
time.Sleep(20 * time.Millisecond)
}
wg.Wait()
ui(func() { log.SetText(log.Text + tr("logAllDone") + "\n"); progress.SetValue(1) })
}
/* =========================
UI References & Apply
========================= */
type UIRefs struct {
w fyne.Window
aboutBtn *widget.Button
selectedPathLabel *widget.Label
selectedPath *widget.Label
selectFolderBtn *widget.Button
selectFileBtn *widget.Button
keyPathLabel *widget.Label
passwordLabel *widget.Label
modeLabel *widget.Label
algoLabel *widget.Label
langLabel *widget.Label
btnChangeKey *widget.Button
modeOptions *widget.RadioGroup
algoOptions *widget.RadioGroup
langSwitch *widget.RadioGroup
encryptBtn *widget.Button
decryptBtn *widget.Button
keyPath *widget.Entry
passwordEntry *widget.Entry
logArea *widget.Entry
progress *widget.ProgressBar
}
func applyLanguage(u *UIRefs, locked *bool, hasTarget *bool, isPasswordMode bool, isAES bool) {
ui(func() {
u.w.SetTitle(tr("windowTitle"))
u.aboutBtn.SetText(tr("aboutBtn"))
u.selectedPathLabel.SetText(tr("targetLabel"))
if !*hasTarget {
u.selectedPath.SetText(tr("noTarget"))
}
u.selectFolderBtn.SetText(tr("selectFolder"))
u.selectFileBtn.SetText(tr("selectFile"))
u.keyPathLabel.SetText(tr("keyPathLabel"))
u.passwordLabel.SetText(tr("passwordLabel"))
u.modeLabel.SetText(tr("modeLabel"))
u.algoLabel.SetText(tr("algoLabel"))
u.langLabel.SetText(tr("langLabel"))
if *locked {
u.btnChangeKey.SetText(tr("changeKey"))
} else {
u.btnChangeKey.SetText(tr("lockKey"))
}
// 更新選項並保持目前的預選狀態
u.modeOptions.Options = []string{tr("modeKeyFile"), tr("modePassword")}
if isPasswordMode {
u.modeOptions.SetSelected(tr("modePassword"))
} else {
u.modeOptions.SetSelected(tr("modeKeyFile"))
}
u.modeOptions.Refresh()
u.algoOptions.Options = []string{tr("algoSecretBox"), tr("algoAES")}
if isAES {
u.algoOptions.SetSelected(tr("algoAES"))
} else {
u.algoOptions.SetSelected(tr("algoSecretBox"))
}
u.algoOptions.Refresh()
u.langSwitch.Options = []string{tr("langZH"), tr("langEN"), tr("langJA"), tr("langDE")}
u.langSwitch.Refresh()
u.encryptBtn.SetText(tr("encryptBtn"))
u.decryptBtn.SetText(tr("decryptBtn"))
})
}
/* =========================
Main
========================= */
func main() {
a := app.New()
a.Settings().SetTheme(darkTextTheme{theme.DefaultTheme()})
// --- Icon 修復區 ---
iconPath := "icon.png"
if _, err := os.Stat(iconPath); err == nil {
if iconRes, err := fyne.LoadResourceFromPath(iconPath); err == nil {
a.SetIcon(iconRes)
} else {
fmt.Printf("警告: 無法讀取圖示資源: %v\n", err)
}
} else {
fmt.Println("提示: 找不到 icon.png 檔案,將使用預設圖示。")
}
currentLang = loadLanguageFromSettings()
var (
usePassword = false
useAES = false
selectedHasTarget = false
keyLocked = true
)
w := a.NewWindow(tr("windowTitle"))
if a.Icon() != nil {
w.SetIcon(a.Icon())
}
aboutBtn := widget.NewButtonWithIcon(tr("aboutBtn"), theme.InfoIcon(), func() {
dialog.ShowInformation(tr("aboutTitle"), tr("aboutContent"), w)
})
logArea := widget.NewMultiLineEntry()
logArea.SetMinRowsVisible(10)
progress := widget.NewProgressBar()
selectedPath := widget.NewLabelWithStyle(tr("noTarget"), fyne.TextAlignLeading, fyne.TextStyle{Italic: true})
selectedPathLabel := widget.NewLabel(tr("targetLabel"))
keyPath := widget.NewEntry()
keyPath.SetText(DefaultKeyFile)
keyPath.Disable()
passwordEntry := widget.NewPasswordEntry()
passwordEntry.Disable()
btnChangeKey := widget.NewButton(tr("changeKey"), nil)
modeLabel := widget.NewLabel(tr("modeLabel"))
algoLabel := widget.NewLabel(tr("algoLabel"))
langLabel := widget.NewLabel(tr("langLabel"))
modeOptions := widget.NewRadioGroup([]string{tr("modeKeyFile"), tr("modePassword")}, func(value string) {
if strings.Contains(value, "密碼") || strings.Contains(value, "Password") || strings.Contains(value, "パスワード") || strings.Contains(value, "Passwort") {
usePassword = true
passwordEntry.Enable()
keyPath.Disable()
keyLocked = true
btnChangeKey.Disable()
} else {
usePassword = false
passwordEntry.Disable()
keyPath.Disable()
keyLocked = true
btnChangeKey.Enable()
keyPath.SetText(resolveDefaultKeyPath(selectedPath.Text, selectedHasTarget))
}
})
modeOptions.Horizontal = true
algoOptions := widget.NewRadioGroup([]string{tr("algoSecretBox"), tr("algoAES")}, func(value string) {
useAES = strings.Contains(value, "AES")
})
algoOptions.Horizontal = true
// ==========================================
// 設定預選項目 (要在 RadioGroup 定義之後)
// ==========================================
modeOptions.SetSelected(tr("modeKeyFile"))
algoOptions.SetSelected(tr("algoSecretBox"))
btnChangeKey.OnTapped = func() {
if usePassword {
dialog.ShowInformation(tr("noticeTitle"), tr("noticePasswordMode"), w)
return
}
if keyLocked {
keyPath.Enable()
keyLocked = false
btnChangeKey.SetText(tr("lockKey"))
dialog.ShowFileSave(func(uc fyne.URIWriteCloser, err error) {
if uc != nil {
p := uc.URI().Path()
_ = uc.Close()
ui(func() { keyPath.SetText(p) })
}
}, w)
} else {
keyPath.Disable()
keyLocked = true
btnChangeKey.SetText(tr("changeKey"))
}
}
selectFolderBtn := widget.NewButtonWithIcon(tr("selectFolder"), theme.FolderOpenIcon(), func() {
dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) {
if uri != nil {
p := uri.Path()
ui(func() {
selectedPath.SetText(p)
selectedHasTarget = true
keyPath.SetText(resolveDefaultKeyPath(p, true))
})
}
}, w)
})
selectFileBtn := widget.NewButtonWithIcon(tr("selectFile"), theme.FileIcon(), func() {
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
if reader != nil {
p := reader.URI().Path()
_ = reader.Close()
ui(func() {
selectedPath.SetText(p)
selectedHasTarget = true
keyPath.SetText(resolveDefaultKeyPath(p, true))
})
}
}, w)
})
encryptBtn := widget.NewButtonWithIcon(tr("encryptBtn"), theme.ConfirmIcon(), nil)
decryptBtn := widget.NewButtonWithIcon(tr("decryptBtn"), theme.ViewRefreshIcon(), nil)
u := &UIRefs{
w: w, aboutBtn: aboutBtn,
selectedPathLabel: selectedPathLabel, selectedPath: selectedPath,
selectFolderBtn: selectFolderBtn, selectFileBtn: selectFileBtn,
keyPathLabel: widget.NewLabel(tr("keyPathLabel")), passwordLabel: widget.NewLabel(tr("passwordLabel")),
modeLabel: modeLabel, algoLabel: algoLabel, langLabel: langLabel,
btnChangeKey: btnChangeKey, modeOptions: modeOptions, algoOptions: algoOptions,
encryptBtn: encryptBtn, decryptBtn: decryptBtn,
keyPath: keyPath, passwordEntry: passwordEntry, logArea: logArea, progress: progress,
}
langSwitch := widget.NewRadioGroup([]string{tr("langZH"), tr("langEN"), tr("langJA"), tr("langDE")}, func(v string) {
if v == tr("langEN") || v == "English" {
currentLang = LangEN
} else if v == tr("langJA") || v == "日本語" {
currentLang = LangJA
} else if v == tr("langDE") || v == "Deutsch" {
currentLang = LangDE
} else {
currentLang = LangZH
}
// 切換語言時,傳入目前的選擇狀態以維持預選
applyLanguage(u, &keyLocked, &selectedHasTarget, usePassword, useAES)
saveLanguageToSettings(currentLang)
})
langSwitch.Horizontal = true
u.langSwitch = langSwitch
if currentLang == LangEN {
langSwitch.SetSelected("English")
} else if currentLang == LangJA {
langSwitch.SetSelected("日本語")
} else if currentLang == LangDE {
langSwitch.SetSelected("Deutsch")
} else {
langSwitch.SetSelected("繁體中文")
}
encryptBtn.OnTapped = func() {
target := selectedPath.Text
if !selectedHasTarget || target == "" {
dialog.ShowInformation(tr("noticeTitle"), tr("noticeSelectTarget"), w)
return
}
fi, err := os.Stat(target)
if os.IsNotExist(err) {
dialog.ShowError(fmt.Errorf(tr("errTargetNotFound")), w)
return
}
var k *[32]byte
if usePassword {
if passwordEntry.Text == "" {
dialog.ShowInformation(tr("noticeTitle"), tr("noticeEnterPassword"), w)
return
}
k = deriveKeyFromPassword(passwordEntry.Text)
} else {
k = loadOrCreateKey(keyPath.Text)
}
go func() {
ui(func() { progress.SetValue(0) })
if fi.IsDir() {
processFolder(target, k, "encrypt", useAES, progress, logArea)
} else {
var err error
if useAES {
err = encryptFileAES(target, k)
} else {
err = encryptFileSecretbox(target, k)
}
ui(func() {
if err != nil {
logArea.SetText(logArea.Text + fmt.Sprintf(tr("logEncryptFailedFmt")+"\n", err))
} else {
logArea.SetText(logArea.Text + fmt.Sprintf(tr("logEncryptDoneFmt")+"\n", target))
}
progress.SetValue(1)
})
}
}()
}
decryptBtn.OnTapped = func() {
target := selectedPath.Text
if !selectedHasTarget || target == "" {
dialog.ShowInformation(tr("noticeTitle"), tr("noticeSelectTarget"), w)
return
}
fi, err := os.Stat(target)
if os.IsNotExist(err) {
dialog.ShowError(fmt.Errorf(tr("errTargetNotFound")), w)
return
}
var k *[32]byte
if usePassword {
if passwordEntry.Text == "" {
dialog.ShowInformation(tr("noticeTitle"), tr("noticeEnterPassword"), w)
return
}
k = deriveKeyFromPassword(passwordEntry.Text)
} else {
k = loadOrCreateKey(keyPath.Text)
}
go func() {
ui(func() { progress.SetValue(0) })
if fi.IsDir() {
processFolder(target, k, "decrypt", useAES, progress, logArea)
} else {
var err error
if useAES {
err = decryptFileAES(target, k)
} else {
err = decryptFileSecretbox(target, k)
}
ui(func() {
if err != nil {
logArea.SetText(logArea.Text + fmt.Sprintf(tr("logDecryptFailedFmt")+"\n", err))
} else {
logArea.SetText(logArea.Text + fmt.Sprintf(tr("logDecryptDoneFmt")+"\n", strings.TrimSuffix(target, ".enc")))
}
progress.SetValue(1)
})
}
}()
}
topBar := container.NewBorder(nil, nil, langLabel, aboutBtn, langSwitch)
targetBox := container.NewVBox(
container.NewHBox(selectedPathLabel, selectedPath),
container.NewHBox(selectFolderBtn, selectFileBtn),
)
cryptoBox := container.NewVBox(
widget.NewSeparator(),
container.NewHBox(modeLabel, modeOptions),
container.NewHBox(algoLabel, algoOptions),
container.NewBorder(nil, nil, u.keyPathLabel, btnChangeKey, keyPath),
container.NewBorder(nil, nil, u.passwordLabel, nil, passwordEntry),
container.NewHBox(layout.NewSpacer(), encryptBtn, decryptBtn, layout.NewSpacer()),
)
content := container.NewBorder(
container.NewVBox(topBar, widget.NewSeparator(), targetBox),
nil, nil, nil,
container.NewVBox(cryptoBox, progress, logArea),
)
w.SetContent(container.NewPadded(content))
w.Resize(fyne.NewSize(850, 650))
w.ShowAndRun()
}
⚠️ 免責聲明(Disclaimer)
作者不對因使用本程式所造成的任何資料遺失、損壞或其他後果負責。
「為了保障你的隱私與安全,PeceZen 的工具堅持不追蹤、不放廣告、完全離線。如果你覺得這些工具為你節省了寶貴的時間,歡迎請我喝杯咖啡,幫助我維持伺服器的運行與工具的更新。」
[ ☕ 請我喝杯咖啡 ]




