Heim >Backend-Entwicklung >Golang >Eine minimalistische Passwort-Manager-Desktop-App: ein Ausflug in das Wails-Framework von Golang (Teil 1)

Eine minimalistische Passwort-Manager-Desktop-App: ein Ausflug in das Wails-Framework von Golang (Teil 1)

Mary-Kate Olsen
Mary-Kate OlsenOriginal
2024-12-20 10:22:10419Durchsuche

I – Warum heute Desktop-Anwendungen entwickeln?

Das ist eine Frage, die sich alle Entwickler gestellt haben, insbesondere wenn sie aus der Webdev-Welt kommen: „Wenn ich fast alles ausführen kann, was im Browser gerendert wird und fast jedem gewünschten Zweck dient, Wer müsste unsere Anwendung herunterladen und auf seinem Computer ausführen?“ Aber abgesehen von den offensichtlichen Anforderungen an die Arbeit, die wir leisten (für uns selbst oder für ein Unternehmen, z. B. die Nutzung aller Betriebssystemfunktionen, bessere Leistung, Offline-Fähigkeiten, verbesserte Sicherheit und Integration usw.), gibt es noch Folgendes: Erfahrungen, die wir als Entwickler durch die Auseinandersetzung mit neuen Aspekten der Programmierung sammeln, die uns immer bereichern werden.

Wenn Sie wie ich eine Leidenschaft für Golang haben und Backends in dieser Sprache entwickelt, aber auch Frontends mit HTML, CSS und JavaScript (oder einigen seiner Frameworks) erstellt haben, ist dieser Beitrag genau das Richtige für Sie, denn ohne dass es nötig ist Um eine neue Technologie zu erlernen, sind Sie bestens in der Lage, Desktop-Anwendungen zu erstellen.

II – Die Antwort heißt Wails

Wahrscheinlich kennen Sie bereits Electron oder Tauri. Beide nutzen Web-Technologien für das Frontend; Das erste verwendet JavaScript (oder besser gesagt NodeJs) in seinem Backend und das zweite verwendet Rust. Aber beide haben mehr oder weniger bemerkenswerte Nachteile. Electron-Apps haben sehr große Binärdateien (da sie einen kompletten Chromium-Browser packen) und verbrauchen viel Speicher. Tauri-Apps verbessern diese Aspekte (unter anderem, weil sie WebView2 [Windows]/WebKit [macOS & Linux] anstelle von Chromium verwenden), aber die Binärdatei ist immer noch relativ groß und ihre Kompilierungszeiten sind … sind die von Rust? (ganz zu schweigen von ihrer Lernkurve, obwohl ich Rust liebe, meine ich es wirklich so?).

Durch die Verwendung von Wails erhalten Sie das Beste aus all diesen Welten der Desktop-Anwendungsentwicklung mit Webtechnologien, die ich gerade beschrieben habe, sowie alle Vorteile, die die Verwendung von Go mit sich bringt:

  • leicht zu erlernende und außergewöhnlich ausdrucksstarke Sprache,
  • Schnelle Ausführung und vor allem schnelle Kompilierung,
  • „out of the box“-Cross-Compilation,
  • Kleine Binärdateien, die mit mäßigem Speicherverbrauch laufen (die Anwendung, die wir hier entwickeln werden, wäre beispielsweise ~100 MB mit Electron und mit anderen nativen GUI-Frameworks wie Fyne ~20 MB; mit Wails sind es nur 4 MB?) !!),
  • und die Möglichkeit, das Web-Framework Ihrer Wahl (sogar Vanilla JS) zu verwenden, mit dem Sie ganz einfach „moderne“ Benutzeroberflächen entwerfen können, die das Benutzererlebnis verbessern.

Ja, wenn ich Go zum Erstellen von Desktop-Anwendungen verwenden möchte, gibt es andere Möglichkeiten (nativ oder nicht). Ich würde Fyne und go-gtk erwähnen. Fyne ist ein GUI-Framework, das die einfache Erstellung nativer Apps ermöglicht. Auch wenn diese ein elegantes Design haben, sind die Fähigkeiten des Frameworks etwas eingeschränkt oder erfordern einen großen Aufwand seitens des Entwicklers, um dasselbe zu erreichen Mit anderen Tools und/oder Sprachen wäre dies problemlos möglich. Das Gleiche kann ich über go-gtk sagen, eine Go-Bindung für GTK: Ja, es ist wahr, dass Sie native Anwendungen erhalten, deren Grenzen in Ihren eigenen Fähigkeiten liegen. Aber der Zugang zur GTK-Bibliothek ist wie eine Expedition durch den Dschungel?…

III – Ein Ansatz für Wails: Nu-i uita – Minimalistischer Passwort-Manager

Zuerst einmal für diejenigen, die sich fragen, was Nu-i uita bedeutet: Auf Rumänisch bedeutet es ungefähr „Vergiss sie nicht“. Ich dachte, es wäre ein origineller Name...

Sie können den gesamten Code der Anwendung in diesem GitHub-Repository sehen. Wenn Sie es gleich ausprobieren möchten, können Sie die ausführbare Datei hier herunterladen (für Windows und Linux).

A minimalist password manager desktop app: a foray into Golang

Ich beschreibe kurz die Funktionsweise der Anwendung: Der Benutzer meldet sich zum ersten Mal an und wird im Anmeldefenster aufgefordert, ein Master-Passwort einzugeben. Dies wird verschlüsselt gespeichert, wobei das Passwort selbst als Verschlüsselungsschlüssel dient. Dieses Anmeldefenster führt zu einer weiteren Oberfläche, auf der der Benutzer die gespeicherten Passwörter für die entsprechenden Websites und verwendeten Benutzernamen auflisten kann (Sie können in dieser Liste auch nach Benutzername oder Website suchen). Sie können auf jedes Element in der Liste klicken und dessen Details anzeigen, es in die Zwischenablage kopieren, bearbeiten oder löschen. Wenn Sie neue Elemente hinzufügen, werden Ihre Passwörter außerdem mit dem Master-Passwort als Schlüssel verschlüsselt. Im Konfigurationsfenster können Sie die Sprache Ihrer Wahl auswählen (derzeit nur Englisch und Spanisch), alle gespeicherten Daten löschen, exportieren oder aus einer Sicherungsdatei importieren. Beim Importieren von Daten wird der Benutzer nach dem Master-Passwort gefragt, das beim Export verwendet wurde, und die importierten Daten werden nun mit dem aktuellen Master-Passwort gespeichert und verschlüsselt. Anschließend wird der Benutzer bei jeder erneuten Anmeldung bei der Anwendung aufgefordert, das aktuelle Master-Passwort einzugeben.

Ich werde nicht weiter auf die Anforderungen eingehen, die Sie für die Verwendung von Wails benötigen, da diese in der hervorragenden Dokumentation gut erklärt werden. In jedem Fall ist es wichtig, dass Sie die leistungsstarke CLI installieren (installieren Sie github.com/wailsapp/wails/v2/cmd/wails@latest), mit der Sie ein Gerüst für die Anwendung generieren und beim Bearbeiten von Code im laufenden Betrieb neu laden können und ausführbare Dateien erstellen (einschließlich Cross-Compilation).

Mit der Wails-CLI können Sie Projekte mit einer Vielzahl von Frontend-Frameworks generieren, aber aus irgendeinem Grund scheinen die Entwickler von Wails Svelte zu bevorzugen ... weil es die erste Option ist, die sie erwähnen. Wenn Sie den Befehl wails init -n myproject -t svelte-ts verwenden, generieren Sie ein Projekt mit Svelte3 und TypeScript.

Wenn Sie aus irgendeinem Grund lieber Svelte5 mit seiner neuen Funktion Runensystem verwenden möchten, habe ich ein Bash-Skript erstellt, das die Generierung von Projekten automatisiert Svelte5. In diesem Fall muss auch die Wails-CLI installiert sein.

Die oben genannten Funktionen der Anwendung stellen die Anforderungen jeder todoapp dar (was immer eine gute Möglichkeit ist, etwas Neues in der Programmierung zu lernen), aber hier fügen wir noch ein Plus hinzu von Funktionen (z. B. sowohl im Backend die Verwendung der symmetrischen Verschlüsselung als auch im Frontend die Verwendung von Internationalisierung), die es etwas nützlicher machen und lehrreicher als eine einfache Todo-App.

Okay, genug der Einleitung, also kommen wir zur Sache?.

IV – Wails-Projektstruktur: Ein Überblick über die Funktionsweise dieses Frameworks

Wenn Sie sich dafür entscheiden, ein Wails-Projekt mit Svelte Typescript mit der CLI zu erstellen, indem Sie den Befehl wails init -n myproject -t svelte-ts ausführen (oder mit dem Bash-Skript, das ich erstellt habe und von dem ich Ihnen bereits zuvor erzählt habe, wird das generiert Wails-Projekte mit Svelte5) erhalten Sie eine Verzeichnisstruktur, die dieser sehr ähnlich ist:

.
├── app.go
├── build
│   ├── appicon.png
│   ├── darwin
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── README.md
│   └── windows
│       ├── icon.ico
│       ├── info.json
│       ├── installer
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── frontend
│   ├── index.html
│   ├── package.json
│   ├── package.json.md5
│   ├── package-lock.json
│   ├── postcss.config.js
│   ├── README.md
│   ├── src
│   │   ├── App.svelte
│   │   ├── assets
│   │   │   ├── fonts
│   │   │   │   ├── nunito-v16-latin-regular.woff2
│   │   │   │   └── OFL.txt
│   │   │   └── images
│   │   │       └── logo-universal.png
│   │   ├── lib
│   │   │   ├── BackBtn.svelte
│   │   │   ├── BottomActions.svelte
│   │   │   ├── EditActions.svelte
│   │   │   ├── EntriesList.svelte
│   │   │   ├── Language.svelte
│   │   │   ├── popups
│   │   │   │   ├── alert-icons.ts
│   │   │   │   └── popups.ts
│   │   │   ├── ShowPasswordBtn.svelte
│   │   │   └── TopActions.svelte
│   │   ├── locales
│   │   │   ├── en.json
│   │   │   └── es.json
│   │   ├── main.ts
│   │   ├── pages
│   │   │   ├── About.svelte
│   │   │   ├── AddPassword.svelte
│   │   │   ├── Details.svelte
│   │   │   ├── EditPassword.svelte
│   │   │   ├── Home.svelte
│   │   │   ├── Login.svelte
│   │   │   └── Settings.svelte
│   │   ├── style.css
│   │   └── vite-env.d.ts
│   ├── svelte.config.js
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   ├── vite.config.ts
│   └── wailsjs
│       ├── go
│       │   ├── main
│       │   │   ├── App.d.ts
│       │   │   └── App.js
│       │   └── models.ts
│       └── runtime
│           ├── package.json
│           ├── runtime.d.ts
│           └── runtime.js
├── go.mod
├── go.sum
├── internal
│   ├── db
│   │   └── db.go
│   └── models
│       ├── crypto.go
│       ├── master_password.go
│       └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json

Was Sie gerade gesehen haben, ist die fertige Bewerbungsstruktur. Der einzige Unterschied zu dem von der Wails-CLI generierten besteht darin, dass Sie damit das Gerüst einer Wails-Anwendung mit einem Svelte3-TypeScript-Frontend erhalten und mein Skript zusätzlich zu Svelte5 auch Tailwindcss Daisyui hat integriert.

Aber sehen wir uns an, wie eine Wails-Anwendung im Allgemeinen funktioniert, und geben wir gleichzeitig die Erklärung für unseren Fall im Einzelnen:

A minimalist password manager desktop app: a foray into Golang

In der Wails-Dokumentation heißt es: „Eine Wails-Anwendung ist eine Standard-Go-Anwendung mit einem Webkit Frontend. Der Go-Teil der Anwendung besteht aus dem Anwendungscode und einer Laufzeitbibliothek das eine Reihe nützlicher Vorgänge bietet, wie z. B. die Steuerung des Anwendungsfensters. Das Frontend ist ein Webkit-Fenster, das die Frontend-Assets anzeigt. Kurz gesagt, und wie wir wahrscheinlich bereits wissen, wenn wir Desktop-Anwendungen mit Web-Technologien erstellt haben, besteht die Anwendung ganz kurz erklärt aus einem Backend (in unserem Fall in Go geschrieben) und einem Frontend, dessen Assets von einem Webkit-Fenster verwaltet werden (in im Fall des Windows-Betriebssystems Webview2), so etwas wie die Essenz eines Webservers/Browsers, der die Frontend-Assets bereitstellt/gerendert.

Die Hauptanwendung in unserem speziellen Fall, in dem wir möchten, dass die Anwendung sowohl unter Windows als auch unter Linux ausgeführt werden kann, besteht aus dem folgenden Code:

/* main.go */

package main

import (
    "embed"

    "github.com/wailsapp/wails/v2"
    "github.com/wailsapp/wails/v2/pkg/options"
    "github.com/wailsapp/wails/v2/pkg/options/assetserver"
    "github.com/wailsapp/wails/v2/pkg/options/linux"
)

//go:embed all:frontend/dist
var assets embed.FS

//go:embed build/appicon.png
var icon []byte

func main() {
    // Create an instance of the app structure
    app := NewApp()

    // Create application with options
    err := wails.Run(&options.App{
        // Title:         "Nu-i uita • minimalist password manager",
        Width:         450,
        Height:        300,
        DisableResize: true,
        AssetServer: &assetserver.Options{
            Assets: assets,
        },
        BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
        OnStartup:        app.startup,
        OnBeforeClose:    app.beforeClose,
        Bind: []interface{}{
            app,
        },
        // Linux platform specific options
        Linux: &linux.Options{
            Icon: icon,
            // WindowIsTranslucent: true,
            WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
            // ProgramName:         "wails",
        },
    })

    if err != nil {
        println("Error:", err.Error())
    }
}

Das erste, was wir tun müssen, ist eine Struktur zu instanziieren (mit der Funktion NewApp), die wir App nennen wollten und die ein Feld mit einem Go Context haben muss. Dann ist die Methode Run von wails diejenige, die die Anwendung startet. Wir müssen ihm eine Reihe von Optionen übergeben. Eine dieser obligatorischen Optionen ist das Vermögen. Sobald Wails das Frontend kompiliert, generiert es es im Ordner „frontend/dist“. Mit der Direktive //go:embed all:frontend/dist (dieser magischen Funktion von Go) können wir unser gesamtes Frontend in die endgültige ausführbare Datei einbetten. Wenn wir im Fall von Linux das Anwendungssymbol einbetten möchten, müssen wir auch die Direktive //go:embed verwenden.

Ich werde nicht auf die restlichen Optionen eingehen, die Sie in der Dokumentation nachlesen können. Ich möchte nur zwei Dinge im Zusammenhang mit den Optionen sagen. Der erste besteht darin, dass der Titel, der in der Titelleiste der Anwendung angezeigt wird, hier als Option festgelegt werden kann. In unserer Anwendung, in der der Benutzer die gewünschte Sprache auswählen kann, werden wir sie jedoch festlegen (mithilfe der Wails-Laufzeit), wenn wir die erhalten Sprachänderungsereignis, das der Benutzer durchführen kann. Wir werden das später sehen.

Das zweite wichtige Optionsproblem ist die Option Bind. Die Dokumentation erklärt ihre Bedeutung sehr gut: „Die Bind-Option ist eine der wichtigsten Optionen in einer Wails-Anwendung. Sie gibt an, welche Strukturmethoden dem Frontend zugänglich gemacht werden sollen. Denken Sie an Strukturen wie Controller in einem traditionellen Web Anwendung." In der Tat: Die öffentlichen Methoden der App-Struktur, die das Backend dem Frontend zugänglich machen, führen die Magie aus, Go mit JavaScript zu „verbinden“. Diese öffentlichen Methoden dieser Struktur werden in JavaScript-Funktionen umgewandelt, die durch die von Wails durchgeführte Kompilierung ein Versprechen zurückgeben.

Die andere wichtige Form der Kommunikation zwischen dem Backend und dem Frontend (die wir in dieser Anwendung effektiv nutzen) sind Ereignisse. Wails bietet ein Ereignissystem, in dem Ereignisse von Go oder JavaScript gesendet oder empfangen werden können. Optional können Daten mit den Ereignissen übergeben werden. Wenn wir die Art und Weise untersuchen, wie wir Ereignisse in unserer Anwendung verwenden, werden wir die Struktur App analysieren:

.
├── app.go
├── build
│   ├── appicon.png
│   ├── darwin
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── README.md
│   └── windows
│       ├── icon.ico
│       ├── info.json
│       ├── installer
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── frontend
│   ├── index.html
│   ├── package.json
│   ├── package.json.md5
│   ├── package-lock.json
│   ├── postcss.config.js
│   ├── README.md
│   ├── src
│   │   ├── App.svelte
│   │   ├── assets
│   │   │   ├── fonts
│   │   │   │   ├── nunito-v16-latin-regular.woff2
│   │   │   │   └── OFL.txt
│   │   │   └── images
│   │   │       └── logo-universal.png
│   │   ├── lib
│   │   │   ├── BackBtn.svelte
│   │   │   ├── BottomActions.svelte
│   │   │   ├── EditActions.svelte
│   │   │   ├── EntriesList.svelte
│   │   │   ├── Language.svelte
│   │   │   ├── popups
│   │   │   │   ├── alert-icons.ts
│   │   │   │   └── popups.ts
│   │   │   ├── ShowPasswordBtn.svelte
│   │   │   └── TopActions.svelte
│   │   ├── locales
│   │   │   ├── en.json
│   │   │   └── es.json
│   │   ├── main.ts
│   │   ├── pages
│   │   │   ├── About.svelte
│   │   │   ├── AddPassword.svelte
│   │   │   ├── Details.svelte
│   │   │   ├── EditPassword.svelte
│   │   │   ├── Home.svelte
│   │   │   ├── Login.svelte
│   │   │   └── Settings.svelte
│   │   ├── style.css
│   │   └── vite-env.d.ts
│   ├── svelte.config.js
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   ├── vite.config.ts
│   └── wailsjs
│       ├── go
│       │   ├── main
│       │   │   ├── App.d.ts
│       │   │   └── App.js
│       │   └── models.ts
│       └── runtime
│           ├── package.json
│           ├── runtime.d.ts
│           └── runtime.js
├── go.mod
├── go.sum
├── internal
│   ├── db
│   │   └── db.go
│   └── models
│       ├── crypto.go
│       ├── master_password.go
│       └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json

Das erste, was wir sehen, ist die Struktur-App, die ein Feld hat, das einen Go Kontext speichert, der von Wails benötigt wird, und einen Zeiger auf die Struktur-Datenbank (in Bezug auf die Datenbank, wie wir sehen werden). . Die anderen beiden Eigenschaften sind Zeichenfolgen, die wir so konfigurieren, dass die nativen Dialoge (vom Backend verwaltet) Titel entsprechend der vom Benutzer ausgewählten Sprache anzeigen. Die Funktion, die als Konstruktor (NewApp) für App fungiert, erstellt einfach den Zeiger auf die Datenbankstruktur.

Als nächstes sehen wir zwei Methoden, die von den Optionen benötigt werden, die Wails benötigt: startup und beforeClose, die wir jeweils an OnStartup und übergeben OnBeforeClose-Optionen. Dadurch erhalten sie automatisch einen Go Context. beforeClose schließt einfach die Verbindung zur Datenbank, wenn die Anwendung geschlossen wird. Aber Startup macht noch mehr. Zunächst legt es den Kontext fest, den es in seinem entsprechenden Feld empfängt. Zweitens registriert es eine Reihe von Ereignis-Listenern im Backend, die wir benötigen, um eine Reihe von Aktionen auszulösen.

In Wails haben alle Event-Listener diese Signatur:

.
├── app.go
├── build
│   ├── appicon.png
│   ├── darwin
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── README.md
│   └── windows
│       ├── icon.ico
│       ├── info.json
│       ├── installer
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── frontend
│   ├── index.html
│   ├── package.json
│   ├── package.json.md5
│   ├── package-lock.json
│   ├── postcss.config.js
│   ├── README.md
│   ├── src
│   │   ├── App.svelte
│   │   ├── assets
│   │   │   ├── fonts
│   │   │   │   ├── nunito-v16-latin-regular.woff2
│   │   │   │   └── OFL.txt
│   │   │   └── images
│   │   │       └── logo-universal.png
│   │   ├── lib
│   │   │   ├── BackBtn.svelte
│   │   │   ├── BottomActions.svelte
│   │   │   ├── EditActions.svelte
│   │   │   ├── EntriesList.svelte
│   │   │   ├── Language.svelte
│   │   │   ├── popups
│   │   │   │   ├── alert-icons.ts
│   │   │   │   └── popups.ts
│   │   │   ├── ShowPasswordBtn.svelte
│   │   │   └── TopActions.svelte
│   │   ├── locales
│   │   │   ├── en.json
│   │   │   └── es.json
│   │   ├── main.ts
│   │   ├── pages
│   │   │   ├── About.svelte
│   │   │   ├── AddPassword.svelte
│   │   │   ├── Details.svelte
│   │   │   ├── EditPassword.svelte
│   │   │   ├── Home.svelte
│   │   │   ├── Login.svelte
│   │   │   └── Settings.svelte
│   │   ├── style.css
│   │   └── vite-env.d.ts
│   ├── svelte.config.js
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   ├── vite.config.ts
│   └── wailsjs
│       ├── go
│       │   ├── main
│       │   │   ├── App.d.ts
│       │   │   └── App.js
│       │   └── models.ts
│       └── runtime
│           ├── package.json
│           ├── runtime.d.ts
│           └── runtime.js
├── go.mod
├── go.sum
├── internal
│   ├── db
│   │   └── db.go
│   └── models
│       ├── crypto.go
│       ├── master_password.go
│       └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json

Das heißt, es erhält den Kontext (den, den wir im ctx-Feld von App speichern), den Namen des Ereignisses (den wir im Frontend festgelegt haben) und einen Callback, der ausgeführt wird die Aktion, die wir brauchen, und die wiederum optionale Parameter vom Typ „any“ oder „leer interface“ (interface{}) empfangen kann, was dasselbe ist, also müssen wir Typzusicherungen machen.

Einige der von uns deklarierten Listener verfügen über deklarierte verschachtelte Ereignisemitter, die im Frontend empfangen werden und dort bestimmte Aktionen auslösen. Seine Signatur sieht so aus:

/* main.go */

package main

import (
    "embed"

    "github.com/wailsapp/wails/v2"
    "github.com/wailsapp/wails/v2/pkg/options"
    "github.com/wailsapp/wails/v2/pkg/options/assetserver"
    "github.com/wailsapp/wails/v2/pkg/options/linux"
)

//go:embed all:frontend/dist
var assets embed.FS

//go:embed build/appicon.png
var icon []byte

func main() {
    // Create an instance of the app structure
    app := NewApp()

    // Create application with options
    err := wails.Run(&options.App{
        // Title:         "Nu-i uita • minimalist password manager",
        Width:         450,
        Height:        300,
        DisableResize: true,
        AssetServer: &assetserver.Options{
            Assets: assets,
        },
        BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
        OnStartup:        app.startup,
        OnBeforeClose:    app.beforeClose,
        Bind: []interface{}{
            app,
        },
        // Linux platform specific options
        Linux: &linux.Options{
            Icon: icon,
            // WindowIsTranslucent: true,
            WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
            // ProgramName:         "wails",
        },
    })

    if err != nil {
        println("Error:", err.Error())
    }
}

Ich werde nicht im Detail darauf eingehen, was diese Listener tun, nicht nur der Kürze halber, sondern auch, weil Go ausdrucksstark genug ist, dass man allein durch das Lesen des Codes erkennen kann, was sie tun. Ich werde nur einige davon erklären. Der Listener „change_titles“ erwartet den Empfang eines Ereignisses mit diesem Namen. Dieses Ereignis wird ausgelöst, wenn der Benutzer die Sprache der Benutzeroberfläche ändert, wodurch der Titel der Titelleiste des Anwendungsfensters in den vom Listener selbst empfangenen Wert geändert wird. Um dies zu erreichen, verwenden wir das Wails-Laufzeitpaket. Das Ereignis erhält außerdem die Titel der Dialogfelder „Verzeichnis auswählen“ und „Datei auswählen“, die in separaten Eigenschaften der App-Struktur gespeichert sind und bei Bedarf verwendet werden können. Wie Sie sehen, benötigen wir dieses Ereignis, da diese „nativen“ Aktionen vom Backend aus ausgeführt werden müssen.

Besondere Erwähnung verdienen die Listener „import_data“ und „password“, die sozusagen verkettet sind. Die erste („import_data“) löst beim Empfang das Öffnen eines Dialogfelds mit der runtime.OpenFileDialog-Methode aus. Wie wir sehen können, erhält diese Methode unter ihren Optionen den anzuzeigenden Titel, der, wie bereits erläutert, im Feld selectedFile der App-Struktur gespeichert wird. Wenn der Benutzer eine Datei auswählt und daher die Variable fileLocation nicht leer ist, wird ein Ereignis ausgegeben (genannt „enter_password“), das im Frontend empfangen wird, um ein Popup anzuzeigen, in dem der Benutzer aufgefordert wird, das von ihm eingegebene Master-Passwort einzugeben verwendet, als er den Export durchführte. Wenn der Benutzer dies tut, gibt das Frontend ein Ereignis („Passwort“) aus, das wir in unserem Backend-Listener empfangen. Die empfangenen Daten (das Master-Passwort) und der Pfad zur Backup-Datei werden von einer Methode der Db-Struktur verwendet, die die Datenbank darstellt (ImportDump). Abhängig vom Ergebnis der Ausführung dieser Methode wird ein neues Ereignis („imported_data“) ausgegeben, das im Frontend ein Popup-Fenster mit dem erfolgreichen oder fehlgeschlagenen Ergebnis des Imports auslöst.

Wie wir sehen können, sind Wails-Events eine leistungsstarke und effektive Art der Kommunikation zwischen dem Backend und dem Frontend.

Die übrigen Methoden der App-Struktur sind nichts anderes als die Methoden, die das Backend dem Frontend zur Verfügung stellt, wie wir bereits erklärt haben, und bei denen es sich im Wesentlichen um CRUD-Operationen mit der Datenbank handelt und die, Deshalb erklären wir es weiter unten.

V – Backend: die Installation der App

Für diesen Backend-Teil habe ich mich von diesem Beitrag (hier auf DEV.to) von vikkio88 und seinem Repo eines Passwort-Managers inspirieren lassen (und einige Änderungen vorgenommen), den er zuerst mit C#/Avalonia erstellt und dann für die Verwendung von Go/ angepasst hat. Fyne (Muscurd-ig).

Der Teil der „untersten Ebene“ des Backends bezieht sich auf die Passwortverschlüsselung. Am wichtigsten sind diese 3 Funktionen:

.
├── app.go
├── build
│   ├── appicon.png
│   ├── darwin
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── README.md
│   └── windows
│       ├── icon.ico
│       ├── info.json
│       ├── installer
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── frontend
│   ├── index.html
│   ├── package.json
│   ├── package.json.md5
│   ├── package-lock.json
│   ├── postcss.config.js
│   ├── README.md
│   ├── src
│   │   ├── App.svelte
│   │   ├── assets
│   │   │   ├── fonts
│   │   │   │   ├── nunito-v16-latin-regular.woff2
│   │   │   │   └── OFL.txt
│   │   │   └── images
│   │   │       └── logo-universal.png
│   │   ├── lib
│   │   │   ├── BackBtn.svelte
│   │   │   ├── BottomActions.svelte
│   │   │   ├── EditActions.svelte
│   │   │   ├── EntriesList.svelte
│   │   │   ├── Language.svelte
│   │   │   ├── popups
│   │   │   │   ├── alert-icons.ts
│   │   │   │   └── popups.ts
│   │   │   ├── ShowPasswordBtn.svelte
│   │   │   └── TopActions.svelte
│   │   ├── locales
│   │   │   ├── en.json
│   │   │   └── es.json
│   │   ├── main.ts
│   │   ├── pages
│   │   │   ├── About.svelte
│   │   │   ├── AddPassword.svelte
│   │   │   ├── Details.svelte
│   │   │   ├── EditPassword.svelte
│   │   │   ├── Home.svelte
│   │   │   ├── Login.svelte
│   │   │   └── Settings.svelte
│   │   ├── style.css
│   │   └── vite-env.d.ts
│   ├── svelte.config.js
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   ├── vite.config.ts
│   └── wailsjs
│       ├── go
│       │   ├── main
│       │   │   ├── App.d.ts
│       │   │   └── App.js
│       │   └── models.ts
│       └── runtime
│           ├── package.json
│           ├── runtime.d.ts
│           └── runtime.js
├── go.mod
├── go.sum
├── internal
│   ├── db
│   │   └── db.go
│   └── models
│       ├── crypto.go
│       ├── master_password.go
│       └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json

Ich werde nicht auf die Details der symmetrischen Verschlüsselung mit AES in Go eingehen. Alles, was Sie wissen müssen, wird in diesem Beitrag hier auf DEV.to ausführlich erklärt.

AES ist ein Blockverschlüsselungsalgorithmus, der einen Schlüssel mit fester Größe und Klartext mit fester Größe verwendet und Chiffretext mit fester Größe zurückgibt. Da die Blockgröße von AES auf 16 Byte festgelegt ist, muss der Klartext mindestens 16 Byte lang sein. Das stellt für uns ein Problem dar, da wir in der Lage sein wollen, Daten beliebiger Größe zu verschlüsseln/entschlüsseln. Um das Problem der minimalen Klartextblockgröße zu lösen, gibt es Blockverschlüsselungsmodi. Hier verwende ich den GCM-Modus, da er einer der am weitesten verbreiteten symmetrischen Blockverschlüsselungsmodi ist. GCM erfordert einen IV (Initialisierungsvektor [Array]), der immer zufällig generiert werden muss (der Begriff für ein solches Array ist Nonce).

Grundsätzlich benötigt die Funktion encrypt einen zu verschlüsselnden Klartext und einen geheimen Schlüssel, der immer 32 Bytes lang sein wird, und generiert mit diesem Schlüssel einen AES-Verschlüsselungscode. Mit diesem Verschlüsseler generieren wir ein gcm-Objekt, das wir verwenden, um einen 12-Byte-Initialisierungsvektor (Nonce) zu erstellen. Die Seal-Methode des gcm-Objekts ermöglicht es uns, den Klartext (als Byte-Slice) mit dem Vektor nonce zu „verknüpfen“, zu verschlüsseln und schließlich das Ergebnis wieder in einen String umzuwandeln.

Die Funktion decrypt bewirkt das Gegenteil: Der erste Teil davon ist gleich encrypt. Da wir dann wissen, dass der Chiffretext tatsächlich Nonce-Chiffretext ist, können wir den Chiffretext in aufteilen seine 2 Komponenten. Die NonceSize-Methode des gcm-Objekts ergibt immer „12“ (die Länge der Nonce), und daher teilen wir das Byte-Slice gleichzeitig mit der Entschlüsselung auf mit der Open-Methode des gcm-Objekts. Abschließend wandeln wir das Ergebnis in einen String um.

Die Funktion keyfy stellt sicher, dass wir einen 32-Byte-Geheimschlüssel haben (indem wir ihn mit „0“ auffüllen, um diese Länge zu erreichen). Wir werden sehen, dass wir im Frontend darauf achten, dass der Benutzer keine Zeichen mit mehr als einem Byte (Nicht-ASCII-Zeichen) eingibt, sodass das Ergebnis dieser Funktion immer 32 Byte lang ist.

Der Rest des Codes in dieser Datei ist im Wesentlichen für die Kodierung/Dekodierung der oben beschriebenen Funktionen in Base64 verantwortlich.

Zur Speicherung aller Anwendungsdaten verwenden wir cloverDB. Es handelt sich um eine leichte und eingebettete dokumentenorientierte NoSQL-Datenbank, ähnlich wie MongoDB. Eines der Merkmale dieser Datenbank besteht darin, dass Datensätzen beim Speichern eine ID zugewiesen wird (standardmäßig wird das Feld als _id bezeichnet, ähnlich wie in MongoDB), bei der es sich um eine uuid-Zeichenfolge handelt ( v4). Wenn wir die Datensätze also nach Eintragsreihenfolge sortieren möchten, müssen wir ihnen beim Speichern einen Zeitstempel zuweisen.

Basierend auf diesen Fakten erstellen wir unsere Modelle und die zugehörigen Methoden (master_password.go & passwort_entry.go):

.
├── app.go
├── build
│   ├── appicon.png
│   ├── darwin
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── README.md
│   └── windows
│       ├── icon.ico
│       ├── info.json
│       ├── installer
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── frontend
│   ├── index.html
│   ├── package.json
│   ├── package.json.md5
│   ├── package-lock.json
│   ├── postcss.config.js
│   ├── README.md
│   ├── src
│   │   ├── App.svelte
│   │   ├── assets
│   │   │   ├── fonts
│   │   │   │   ├── nunito-v16-latin-regular.woff2
│   │   │   │   └── OFL.txt
│   │   │   └── images
│   │   │       └── logo-universal.png
│   │   ├── lib
│   │   │   ├── BackBtn.svelte
│   │   │   ├── BottomActions.svelte
│   │   │   ├── EditActions.svelte
│   │   │   ├── EntriesList.svelte
│   │   │   ├── Language.svelte
│   │   │   ├── popups
│   │   │   │   ├── alert-icons.ts
│   │   │   │   └── popups.ts
│   │   │   ├── ShowPasswordBtn.svelte
│   │   │   └── TopActions.svelte
│   │   ├── locales
│   │   │   ├── en.json
│   │   │   └── es.json
│   │   ├── main.ts
│   │   ├── pages
│   │   │   ├── About.svelte
│   │   │   ├── AddPassword.svelte
│   │   │   ├── Details.svelte
│   │   │   ├── EditPassword.svelte
│   │   │   ├── Home.svelte
│   │   │   ├── Login.svelte
│   │   │   └── Settings.svelte
│   │   ├── style.css
│   │   └── vite-env.d.ts
│   ├── svelte.config.js
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   ├── vite.config.ts
│   └── wailsjs
│       ├── go
│       │   ├── main
│       │   │   ├── App.d.ts
│       │   │   └── App.js
│       │   └── models.ts
│       └── runtime
│           ├── package.json
│           ├── runtime.d.ts
│           └── runtime.js
├── go.mod
├── go.sum
├── internal
│   ├── db
│   │   └── db.go
│   └── models
│       ├── crypto.go
│       ├── master_password.go
│       └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json

MasterPassword verfügt über ein privates Feld (clear), das nicht in/aus der Datenbank gespeichert/abgerufen wird (daher kein Clover-Tag), d. h. es befindet sich nur im Speicher und wird nicht auf der Festplatte gespeichert. Diese Eigenschaft ist das unverschlüsselte Master-Passwort selbst und wird als Verschlüsselungsschlüssel für Passworteingaben verwendet. Dieser Wert wird durch einen Setter im MasterPassword-Objekt gespeichert oder (durch einen Callback) als nicht exportiertes (privates) Feld in der Strukturdatenbank des gleichnamigen Pakets festgelegt (db.go). Für die Passworteingabe verwenden wir zwei Strukturen, eine, die das verschlüsselte Passwort nicht enthält, und eine andere, in der das Passwort bereits verschlüsselt ist. Dabei handelt es sich um das Objekt, das tatsächlich in der Datenbank gespeichert wird (ähnlich einem DTO). , Datenübertragungsobjekt). Die Verschlüsselungs-/Entschlüsselungsmethoden beider Strukturen verwenden intern ein Crypto-Objekt, das über eine Eigenschaft mit dem Verschlüsselungsschlüssel verfügt (das ist das in einen 32 Byte langen Slice umgewandelte Master-Passwort):

/* main.go */

package main

import (
    "embed"

    "github.com/wailsapp/wails/v2"
    "github.com/wailsapp/wails/v2/pkg/options"
    "github.com/wailsapp/wails/v2/pkg/options/assetserver"
    "github.com/wailsapp/wails/v2/pkg/options/linux"
)

//go:embed all:frontend/dist
var assets embed.FS

//go:embed build/appicon.png
var icon []byte

func main() {
    // Create an instance of the app structure
    app := NewApp()

    // Create application with options
    err := wails.Run(&options.App{
        // Title:         "Nu-i uita • minimalist password manager",
        Width:         450,
        Height:        300,
        DisableResize: true,
        AssetServer: &assetserver.Options{
            Assets: assets,
        },
        BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
        OnStartup:        app.startup,
        OnBeforeClose:    app.beforeClose,
        Bind: []interface{}{
            app,
        },
        // Linux platform specific options
        Linux: &linux.Options{
            Icon: icon,
            // WindowIsTranslucent: true,
            WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
            // ProgramName:         "wails",
        },
    })

    if err != nil {
        println("Error:", err.Error())
    }
}

Master-Passwort verfügt über drei Methoden, die eine wichtige Rolle bei der Datenspeicherung/-wiederherstellung spielen:

/* app.go */

package main

import (
    "context"

    "github.com/emarifer/Nu-i-uita/internal/db"
    "github.com/emarifer/Nu-i-uita/internal/models"
    "github.com/wailsapp/wails/v2/pkg/runtime"
)

// App struct
type App struct {
    ctx               context.Context
    db                *db.Db
    selectedDirectory string
    selectedFile      string
}

// NewApp creates a new App application struct
func NewApp() *App {
    db := db.NewDb()

    return &App{db: db}
}

// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
    var fileLocation string
    a.ctx = ctx

    runtime.EventsOn(a.ctx, "change_titles", func(optionalData ...interface{}) {
        if appTitle, ok := optionalData[0].(string); ok {
            runtime.WindowSetTitle(a.ctx, appTitle)
        }
        if selectedDirectory, ok := optionalData[1].(string); ok {
            a.selectedDirectory = selectedDirectory
        }
        if selectedFile, ok := optionalData[2].(string); ok {
            a.selectedFile = selectedFile
        }
    })

    runtime.EventsOn(a.ctx, "quit", func(optionalData ...interface{}) {
        runtime.Quit(a.ctx)
    })

    runtime.EventsOn(a.ctx, "export_data", func(optionalData ...interface{}) {
        d, _ := runtime.OpenDirectoryDialog(a.ctx, runtime.
            OpenDialogOptions{
            Title: a.selectedDirectory,
        })

        if d != "" {
            f, err := a.db.GenerateDump(d)
            if err != nil {
                runtime.EventsEmit(a.ctx, "saved_as", err.Error())
                return
            }
            runtime.EventsEmit(a.ctx, "saved_as", f)
        }
    })

    runtime.EventsOn(a.ctx, "import_data", func(optionalData ...interface{}) {
        fileLocation, _ = runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
            Title: a.selectedFile,
        })

        // fmt.Println("SELECTED FILE:", fileLocation)
        if fileLocation != "" {
            runtime.EventsEmit(a.ctx, "enter_password")
        }
    })

    runtime.EventsOn(a.ctx, "password", func(optionalData ...interface{}) {
        // fmt.Printf("MY PASS: %v", optionalData...)
        if pass, ok := optionalData[0].(string); ok {
            if len(fileLocation) != 0 {
                err := a.db.ImportDump(pass, fileLocation)
                if err != nil {
                    runtime.EventsEmit(a.ctx, "imported_data", err.Error())
                    return
                }
                runtime.EventsEmit(a.ctx, "imported_data", "success")
            }
        }
    })
}

// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
    defer a.db.Close()

    return false
}

...

Mit

GetCrypto können Sie die aktuelle Instanz des Crypto-Objekts abrufen, damit das db.go-Paket Kennworteinträge verschlüsseln/entschlüsseln kann. SetClear ist der zuvor erwähnte Setter und Check ist die Funktion, die überprüft, ob das vom Benutzer eingegebene Master-Passwort korrekt ist; Wie wir sehen können, benötigt es zusätzlich zum Passwort als Argument einen Callback, der je nach Fall der oben genannte Setter sein wird (wenn wir Daten aus der Sicherungsdatei importieren). ) oder die Methode SetMasterPassword des db.go-Pakets, die den Wert im privaten Feld der Db-Struktur festlegt, wenn sich der Benutzer anmeldet in.

Ich werde nicht alle Methoden des db.go-Pakets im Detail erklären, da der größte Teil seines Codes mit der Arbeitsweise mit cloverDB zusammenhängt, was Sie in der Dokumentation nachlesen können. obwohl ich bereits einige wichtige Dinge erwähnt habe, die hier verwendet werden.

.
├── app.go
├── build
│   ├── appicon.png
│   ├── darwin
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── README.md
│   └── windows
│       ├── icon.ico
│       ├── info.json
│       ├── installer
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── frontend
│   ├── index.html
│   ├── package.json
│   ├── package.json.md5
│   ├── package-lock.json
│   ├── postcss.config.js
│   ├── README.md
│   ├── src
│   │   ├── App.svelte
│   │   ├── assets
│   │   │   ├── fonts
│   │   │   │   ├── nunito-v16-latin-regular.woff2
│   │   │   │   └── OFL.txt
│   │   │   └── images
│   │   │       └── logo-universal.png
│   │   ├── lib
│   │   │   ├── BackBtn.svelte
│   │   │   ├── BottomActions.svelte
│   │   │   ├── EditActions.svelte
│   │   │   ├── EntriesList.svelte
│   │   │   ├── Language.svelte
│   │   │   ├── popups
│   │   │   │   ├── alert-icons.ts
│   │   │   │   └── popups.ts
│   │   │   ├── ShowPasswordBtn.svelte
│   │   │   └── TopActions.svelte
│   │   ├── locales
│   │   │   ├── en.json
│   │   │   └── es.json
│   │   ├── main.ts
│   │   ├── pages
│   │   │   ├── About.svelte
│   │   │   ├── AddPassword.svelte
│   │   │   ├── Details.svelte
│   │   │   ├── EditPassword.svelte
│   │   │   ├── Home.svelte
│   │   │   ├── Login.svelte
│   │   │   └── Settings.svelte
│   │   ├── style.css
│   │   └── vite-env.d.ts
│   ├── svelte.config.js
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   ├── vite.config.ts
│   └── wailsjs
│       ├── go
│       │   ├── main
│       │   │   ├── App.d.ts
│       │   │   └── App.js
│       │   └── models.ts
│       └── runtime
│           ├── package.json
│           ├── runtime.d.ts
│           └── runtime.js
├── go.mod
├── go.sum
├── internal
│   ├── db
│   │   └── db.go
│   └── models
│       ├── crypto.go
│       ├── master_password.go
│       └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json

Zuerst haben wir die Struktur, die einen Zeiger auf die cloverDB-Instanz speichert. Außerdem wird ein Zeiger auf eine „vollständige“ Instanz der Struktur MasterPassword gespeichert. „Vollständig“ bedeutet hier, dass sowohl das verschlüsselte Master-Passwort (das heißt, es existiert in der Datenbank und ist daher das aktuelle Master-Passwort) als auch das unverschlüsselte Master-Passwort gespeichert werden, das zur Verschlüsselung der Passworteingaben verwendet wird. Als nächstes haben wir setupCollections, NewDb und Close, das sind Funktionen und Methoden zum Einrichten der Datenbank, wenn die Anwendung gestartet und geschlossen wird. cloverDB erstellt nicht automatisch eine Speicherdatei/ein Speicherverzeichnis, wenn es mit der Open-Methode instanziiert wird, sondern wir müssen es manuell erstellen. Schließlich sind GetLanguageCode und SaveLanguageCode Methoden zum Abrufen/Speichern der vom Benutzer ausgewählten Anwendungssprache. Da es sich bei dem ausgewählten Sprachcode um eine kleine Zeichenfolge („en“ oder „es“) handelt, verwenden wir der Einfachheit halber keine Struktur zum Speichern: um beispielsweise den Sprachcode aus der Sammlung abzurufen (cloverDB funktioniert mit „documents“). und „Sammlungen“, ähnlich wie MongoDB), übergeben wir einfach den Schlüssel, unter dem es gespeichert ist („Code“) und erstellen eine Typzusicherung.

Wenn sich der Benutzer zum ersten Mal anmeldet, wird das Master-Passwort in der Datenbank gespeichert: Dieser Wert (unverschlüsselt) wird im leeren Feld des Objekts MasterPassword festgelegt, in dem auch das bereits verschlüsselte gespeichert wird Passwort und wird in der Db-Struktur gespeichert:

/* main.go */

package main

import (
    "embed"

    "github.com/wailsapp/wails/v2"
    "github.com/wailsapp/wails/v2/pkg/options"
    "github.com/wailsapp/wails/v2/pkg/options/assetserver"
    "github.com/wailsapp/wails/v2/pkg/options/linux"
)

//go:embed all:frontend/dist
var assets embed.FS

//go:embed build/appicon.png
var icon []byte

func main() {
    // Create an instance of the app structure
    app := NewApp()

    // Create application with options
    err := wails.Run(&options.App{
        // Title:         "Nu-i uita • minimalist password manager",
        Width:         450,
        Height:        300,
        DisableResize: true,
        AssetServer: &assetserver.Options{
            Assets: assets,
        },
        BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
        OnStartup:        app.startup,
        OnBeforeClose:    app.beforeClose,
        Bind: []interface{}{
            app,
        },
        // Linux platform specific options
        Linux: &linux.Options{
            Icon: icon,
            // WindowIsTranslucent: true,
            WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
            // ProgramName:         "wails",
        },
    })

    if err != nil {
        println("Error:", err.Error())
    }
}

Die Wiederherstellung des Master-Passworts wird beim Starten der Anwendung zweimal durchgeführt:

  1. um zu überprüfen, ob in der Datenbank ein Master-Passwort gespeichert ist.
  2. um eine MasterPassword-Instanz zu erhalten und deren Check-Methode verwenden und überprüfen zu können, ob das vom Benutzer angegebene Passwort korrekt ist.

In beiden Fällen wird die Methode RecoverMasterPassword aufgerufen, die nur dann, wenn ein Master-Passwort gespeichert ist, die Instanz im Feld „cachedMp“ der Datenbankstruktur festlegt:

/* app.go */

package main

import (
    "context"

    "github.com/emarifer/Nu-i-uita/internal/db"
    "github.com/emarifer/Nu-i-uita/internal/models"
    "github.com/wailsapp/wails/v2/pkg/runtime"
)

// App struct
type App struct {
    ctx               context.Context
    db                *db.Db
    selectedDirectory string
    selectedFile      string
}

// NewApp creates a new App application struct
func NewApp() *App {
    db := db.NewDb()

    return &App{db: db}
}

// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
    var fileLocation string
    a.ctx = ctx

    runtime.EventsOn(a.ctx, "change_titles", func(optionalData ...interface{}) {
        if appTitle, ok := optionalData[0].(string); ok {
            runtime.WindowSetTitle(a.ctx, appTitle)
        }
        if selectedDirectory, ok := optionalData[1].(string); ok {
            a.selectedDirectory = selectedDirectory
        }
        if selectedFile, ok := optionalData[2].(string); ok {
            a.selectedFile = selectedFile
        }
    })

    runtime.EventsOn(a.ctx, "quit", func(optionalData ...interface{}) {
        runtime.Quit(a.ctx)
    })

    runtime.EventsOn(a.ctx, "export_data", func(optionalData ...interface{}) {
        d, _ := runtime.OpenDirectoryDialog(a.ctx, runtime.
            OpenDialogOptions{
            Title: a.selectedDirectory,
        })

        if d != "" {
            f, err := a.db.GenerateDump(d)
            if err != nil {
                runtime.EventsEmit(a.ctx, "saved_as", err.Error())
                return
            }
            runtime.EventsEmit(a.ctx, "saved_as", f)
        }
    })

    runtime.EventsOn(a.ctx, "import_data", func(optionalData ...interface{}) {
        fileLocation, _ = runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
            Title: a.selectedFile,
        })

        // fmt.Println("SELECTED FILE:", fileLocation)
        if fileLocation != "" {
            runtime.EventsEmit(a.ctx, "enter_password")
        }
    })

    runtime.EventsOn(a.ctx, "password", func(optionalData ...interface{}) {
        // fmt.Printf("MY PASS: %v", optionalData...)
        if pass, ok := optionalData[0].(string); ok {
            if len(fileLocation) != 0 {
                err := a.db.ImportDump(pass, fileLocation)
                if err != nil {
                    runtime.EventsEmit(a.ctx, "imported_data", err.Error())
                    return
                }
                runtime.EventsEmit(a.ctx, "imported_data", "success")
            }
        }
    })
}

// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
    defer a.db.Close()

    return false
}

...

Als nächstes gibt es zwei kleine, aber wichtige Codeteile:

  1. SetMasterPassword, das, wie bereits erwähnt, als Callback zur Check-Methode des MasterPassword-Objekts verwendet wird und die unverschlüsselte Einstellung vornimmt Master-Passwort im Feld „cachedMp“ der Datenbankstruktur nur dann, wenn dieses Feld nicht Null ist.
  2. getCryptoInstance, das nur dann eine Instanz des Crypto-Objekts zurückgibt, wenn CachedMp nicht Null ist. Andernfalls wird die Anwendung in Panik versetzt: Obwohl diese Situation theoretisch nicht eintreten kann, wenn der Benutzer in der Anwendung authentifiziert ist, beenden wir aus Sicherheitsgründen die Anwendung, wenn dies geschieht:
EventsOn(
    ctx context.Context,
    eventName string,
    callback func(optionalData ...interface{}),
) func()

Über die üblichen CRUD-Operationen hinaus, die für jede todoapp typisch sind, gibt es noch weitere Funktionen oder Methoden, die wir kommentieren können:

.
├── app.go
├── build
│   ├── appicon.png
│   ├── darwin
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── README.md
│   └── windows
│       ├── icon.ico
│       ├── info.json
│       ├── installer
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── frontend
│   ├── index.html
│   ├── package.json
│   ├── package.json.md5
│   ├── package-lock.json
│   ├── postcss.config.js
│   ├── README.md
│   ├── src
│   │   ├── App.svelte
│   │   ├── assets
│   │   │   ├── fonts
│   │   │   │   ├── nunito-v16-latin-regular.woff2
│   │   │   │   └── OFL.txt
│   │   │   └── images
│   │   │       └── logo-universal.png
│   │   ├── lib
│   │   │   ├── BackBtn.svelte
│   │   │   ├── BottomActions.svelte
│   │   │   ├── EditActions.svelte
│   │   │   ├── EntriesList.svelte
│   │   │   ├── Language.svelte
│   │   │   ├── popups
│   │   │   │   ├── alert-icons.ts
│   │   │   │   └── popups.ts
│   │   │   ├── ShowPasswordBtn.svelte
│   │   │   └── TopActions.svelte
│   │   ├── locales
│   │   │   ├── en.json
│   │   │   └── es.json
│   │   ├── main.ts
│   │   ├── pages
│   │   │   ├── About.svelte
│   │   │   ├── AddPassword.svelte
│   │   │   ├── Details.svelte
│   │   │   ├── EditPassword.svelte
│   │   │   ├── Home.svelte
│   │   │   ├── Login.svelte
│   │   │   └── Settings.svelte
│   │   ├── style.css
│   │   └── vite-env.d.ts
│   ├── svelte.config.js
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   ├── vite.config.ts
│   └── wailsjs
│       ├── go
│       │   ├── main
│       │   │   ├── App.d.ts
│       │   │   └── App.js
│       │   └── models.ts
│       └── runtime
│           ├── package.json
│           ├── runtime.d.ts
│           └── runtime.js
├── go.mod
├── go.sum
├── internal
│   ├── db
│   │   └── db.go
│   └── models
│       ├── crypto.go
│       ├── master_password.go
│       └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json

loadPasswordEntryDTO ist eine Hilfsfunktion, die ein PasswordEntryDTO-Objekt aus einem einzelnen Dokument erstellt, das von cloverDB erhalten wurde. loadManyPasswordEntryDTO macht dasselbe, generiert jedoch aus einem Teil von cloverDB-Dokumenten ein Teil von PasswordEntryDTO. Schließlich macht loadManyPasswordEntry dasselbe wie loadManyPasswordEntryDTO, entschlüsselt aber auch Dokumente, die von cloverDB aus einer Instanz des Crypto-Objekts abgerufen wurden, das von getCryptoInstance generiert wurde Methode.

Schließlich haben wir unter den Methoden, die nichts mit CRUD zu tun haben, diejenigen, die beim Export/Import von Daten verwendet werden:

/* main.go */

package main

import (
    "embed"

    "github.com/wailsapp/wails/v2"
    "github.com/wailsapp/wails/v2/pkg/options"
    "github.com/wailsapp/wails/v2/pkg/options/assetserver"
    "github.com/wailsapp/wails/v2/pkg/options/linux"
)

//go:embed all:frontend/dist
var assets embed.FS

//go:embed build/appicon.png
var icon []byte

func main() {
    // Create an instance of the app structure
    app := NewApp()

    // Create application with options
    err := wails.Run(&options.App{
        // Title:         "Nu-i uita • minimalist password manager",
        Width:         450,
        Height:        300,
        DisableResize: true,
        AssetServer: &assetserver.Options{
            Assets: assets,
        },
        BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
        OnStartup:        app.startup,
        OnBeforeClose:    app.beforeClose,
        Bind: []interface{}{
            app,
        },
        // Linux platform specific options
        Linux: &linux.Options{
            Icon: icon,
            // WindowIsTranslucent: true,
            WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
            // ProgramName:         "wails",
        },
    })

    if err != nil {
        println("Error:", err.Error())
    }
}

GenerateDump verwendet die Struktur DbDump, die das in der Sicherungsdatei gespeicherte Objekt ist. Der Name übernimmt den Pfad des vom Benutzer ausgewählten Verzeichnisses, ein Datumsformat und eine Ad-hoc-Erweiterung. Dann erstellen wir eine DbDump-Instanz mit dem verschlüsselten Master-Passwort, dem DTOs-Slice (mit den entsprechenden ebenfalls verschlüsselten Passwörtern) und dem vom Benutzer in der Datenbank gespeicherten Sprachcode. Dieses Objekt wird schließlich vom Golang-Gob-Paket in der von uns erstellten Datei binär codiert und gibt den Namen der Datei an die Benutzeroberfläche zurück, um den Benutzer über die erfolgreiche Erstellung zu informieren.

Andererseits verwendet ImportDump als Argumente das Master-Passwort, nach dem die Benutzeroberfläche den Benutzer fragt, also das Passwort, das zum Zeitpunkt des Exports gültig war, und den Pfad zur Sicherungsdatei. Nun entschlüsselt es die ausgewählte Datei mithilfe der DbDump-Struktur und ruft dann eine MasterPassword-Instanz aus dem verschlüsselten Master-Passwort ab, das in DbDump gespeichert ist. Im nächsten Schritt überprüfen wir, ob das vom Benutzer angegebene Passwort korrekt ist, während wir das Clear-Feld in der MasterPassword-Instanz festlegen:

/* app.go */

package main

import (
    "context"

    "github.com/emarifer/Nu-i-uita/internal/db"
    "github.com/emarifer/Nu-i-uita/internal/models"
    "github.com/wailsapp/wails/v2/pkg/runtime"
)

// App struct
type App struct {
    ctx               context.Context
    db                *db.Db
    selectedDirectory string
    selectedFile      string
}

// NewApp creates a new App application struct
func NewApp() *App {
    db := db.NewDb()

    return &App{db: db}
}

// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
    var fileLocation string
    a.ctx = ctx

    runtime.EventsOn(a.ctx, "change_titles", func(optionalData ...interface{}) {
        if appTitle, ok := optionalData[0].(string); ok {
            runtime.WindowSetTitle(a.ctx, appTitle)
        }
        if selectedDirectory, ok := optionalData[1].(string); ok {
            a.selectedDirectory = selectedDirectory
        }
        if selectedFile, ok := optionalData[2].(string); ok {
            a.selectedFile = selectedFile
        }
    })

    runtime.EventsOn(a.ctx, "quit", func(optionalData ...interface{}) {
        runtime.Quit(a.ctx)
    })

    runtime.EventsOn(a.ctx, "export_data", func(optionalData ...interface{}) {
        d, _ := runtime.OpenDirectoryDialog(a.ctx, runtime.
            OpenDialogOptions{
            Title: a.selectedDirectory,
        })

        if d != "" {
            f, err := a.db.GenerateDump(d)
            if err != nil {
                runtime.EventsEmit(a.ctx, "saved_as", err.Error())
                return
            }
            runtime.EventsEmit(a.ctx, "saved_as", f)
        }
    })

    runtime.EventsOn(a.ctx, "import_data", func(optionalData ...interface{}) {
        fileLocation, _ = runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
            Title: a.selectedFile,
        })

        // fmt.Println("SELECTED FILE:", fileLocation)
        if fileLocation != "" {
            runtime.EventsEmit(a.ctx, "enter_password")
        }
    })

    runtime.EventsOn(a.ctx, "password", func(optionalData ...interface{}) {
        // fmt.Printf("MY PASS: %v", optionalData...)
        if pass, ok := optionalData[0].(string); ok {
            if len(fileLocation) != 0 {
                err := a.db.ImportDump(pass, fileLocation)
                if err != nil {
                    runtime.EventsEmit(a.ctx, "imported_data", err.Error())
                    return
                }
                runtime.EventsEmit(a.ctx, "imported_data", "success")
            }
        }
    })
}

// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
    defer a.db.Close()

    return false
}

...

Schließlich erhalten wir eine Instanz des Crypto-Objekts aus der im vorherigen Schritt erstellten MasterPasword-Instanz und führen in der folgenden Schleife zwei Dinge aus:

  1. Wir entschlüsseln die DTOs und konvertieren sie in PasswordEntry mit dem bereits entschlüsselten Passwort und
  2. Wir fügen die PasswordEntry's in die Datenbank ein, die nun mit dem neuen Master-Passwort verschlüsselt wird.

Als letztes müssen wir noch den Sprachcode in der Datenbank speichern.

Und das reicht für heute, dieses Tutorial ist schon lang geworden ?‍?.

Im zweiten Teil werden wir die Frontend-Seite detailliert beschreiben, die, wie gesagt, mit Svelte erstellt wurde.

Wenn Sie ungeduldig sind, wie ich Ihnen bereits gesagt habe, finden Sie den gesamten Code in diesem Repo.

Wir sehen uns im zweiten Teil. Viel Spaß beim Codieren ?!!

Das obige ist der detaillierte Inhalt vonEine minimalistische Passwort-Manager-Desktop-App: ein Ausflug in das Wails-Framework von Golang (Teil 1). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn