This is a question that all developers have asked themselves, especially if they come from the webdev world: "If I can run almost anything that will render in the browser and serve almost any purpose I want, who would need to download our application and run it on their computer?". But aside from the obvious requirement of the work we are doing (for ourselves or for a company, e.g. being able to make use of all the OS features, better performance, offline capabilities, improved security and integration, etc.), there is the experience that we as developers gain from touching new aspects of programming that will always enrich us.
If you are passionate about Golang, like me, and you have developed backend in this language, but you have also done frontend with HTML, CSS and JavaScript (or some of its frameworks) this post is for you, because without needing to learn a new technology you are more than capable of creating desktop applications.
Chances are you already know Electron or Tauri. Both use web technologies for the frontend; the first one uses JavaScript (or rather, NodeJs) in its backend, and the second one uses Rust. But both have more or less notable drawbacks. Electron apps have very large binaries (because they package an entire Chromium browser) and consume a lot of memory. Tauri apps improve these aspects (among other things, because they use WebView2 [Windows]/WebKit [macOS & Linux] instead of Chromium), but the binary is still relatively large and their compilation times are… are those of Rust ? (not to mention their learning curve, although I love Rust, I really mean it ?).
By using Wails, you get the best of all these worlds of desktop application development with web technologies that I just described, plus all the advantages that come with using Go:
Yes, if I wanted to use Go to create desktop applications there are other possibilities (native or not). I would mention Fyne and go-gtk. Fyne is a GUI framework that allows the creation of native apps easily and although they may have an elegant design, the capabilities of the framework are somewhat limited or require a great effort from the developer to achieve the same thing that other tools and/or languages would allow you to do easily. I can say the same about go-gtk, which is a Go binding for GTK: yes, it is true that you will get native applications whose limits will be in your own capabilities, but getting into the GTK library is like going on an expedition through the jungle ?…
First of all, for those who are wondering what Nu-i uita means: in Romanian it roughly means "don't forget them". I thought it was an original name...
You can see the entire code of the application in this GitHub repository. If you want to try it out right away, you can download the executable from here (for Windows & Linux).
I will briefly describe how the application works: the user logs in for the first time and the login window asks him to enter a master password. This is saved encrypted using the password itself as the encryption key. This login window leads to another interface where the user can list the saved passwords for the corresponding websites and usernames used (you can also search in this list by username or website). You can click on each item in the list and see its details, copy it to the clipboard, edit it or delete it. Also, when adding new items it will encrypt your passwords using the master password as the key. In the configuration window you can choose the language of your choice (currently only English and Spanish), delete all stored data, export it or import it from a backup file. When importing data, the user will be asked for the master password that was used when the export was performed, and the imported data will now be saved and encrypted with the current master password. Subsequently, whenever the user logs back into the application, he or she will be prompted to enter the current master password.
I am not going to dwell on the requirements you need to use Wails because it is well explained in its excellent documentation. In any case, it is essential that you install its powerful CLI (go install github.com/wailsapp/wails/v2/cmd/wails@latest), which allows you to generate a scaffolding for the application, hot-reload when editing code, and build executables (including cross-compilation).
The Wails CLI allows you to generate projects with a variety of frontend frameworks, but for some reason the creators of Wails seem to prefer Svelte... because it's the first option they mention. When you use the command wails init -n myproject -t svelte-ts, you generate a project with Svelte3 and TypeScript.
If for some reason you prefer to use Svelte5 with its new runes system feature, I have created a bash script that automates the generation of projects with Svelte5. In this case, you will also need to have the Wails CLI installed.
The features of the application I mentioned above constitute the requirements of any todoapp (which is always a good way to learn something new in programming), but here we add a plus of features (e.g. both in the backend, the use of symmetric encryption, and in the frontend, use of Internationalization) that make it a little more useful and instructive than a simple todoapp.
Ok, enough of the introduction, so let's get down to business ?.
If you choose to create a Wails project with Svelte Typescript with the CLI by running the command wails init -n myproject -t svelte-ts (or with the bash script I created and that I already told you about before, that generates Wails projects with Svelte5) you will have a directory structure very similar to this one:
. ├── 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
What you just saw is the finished application structure. The only difference with the one generated by the Wails CLI is that with it you will get the scaffolding of a Wails application with a Svelte3 TypeScript frontend, and with my script, in addition to having Svelte5, Tailwindcss Daisyui is integrated.
But let's see how a Wails application works in general and at the same time particularizing the explanation for our case:
As Wails documentation says: "A Wails application is a standard Go application, with a webkit frontend. The Go part of the application consists of the application code and a runtime library that provides a number of useful operations, like controlling the application window. The frontend is a webkit window that will display the frontend assets". In short, and as we probably already know if we have created desktop applications with web technologies, very briefly explained, the application consists of a backend (in our case written in Go) and a frontend whose assets are managed by a Webkit window (in the case of Windows OS, Webview2), something like the essence of a web server/browser that serves/renders the frontend assets.
The main application in our specific case in which we want the application to be able to run on both Windows and Linux consists of the following 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()) } }
The first thing we need to do is instantiate a struct (with the NewApp function), which we agreed to call App, which must have a field with a Go Context. Then, wails' Run method is the one that starts the application. We need to pass it a series of options. One of these mandatory options is the assets. Once Wails compiles the frontend, it generates it in the "frontend/dist" folder. Using the //go:embed all:frontend/dist directive (that magical feature of Go) we can embed our entire frontend in the final executable. In the case of Linux, if we want to embed the application icon, we must also use the //go:embed directive.
I won't go into the rest of the options, which you can check in the documentation. I'll just say two things related to the options. The first is that the title that appears in the application's title bar can be set here as an option, but in our application, where the user can choose the language they want, we will set them (using the Wails runtime) when we receive the language change event that the user may make. We'll see this later.
The second important option-related issue is the Bind option. The documentation explains its meaning very well: "The Bind option is one of the most important options in a Wails application. It specifies which struct methods to expose to the frontend. Think of structs like controllers in a traditional web application." Indeed: the public methods of the App structure, which are those that expose the backend to the frontend, perform the magic of "connecting" Go with JavaScript. Those public methods of said struct are converted into JavaScript functions that return a promise by the compilation performed by Wails.
The other important form of communication between the backend and the frontend (which we use effectively in this application) is events. Wails provides an event system, where events can be emitted or received by Go or JavaScript. Optionally, data can be passed with the events. Studying the way we use events in our application will lead us to analyze the struct App:
. ├── 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
The first thing we see is the struct App which has a field that stores a Go Context, needed by Wails, and a pointer to the struct Db (related to the database, as we will see). The other 2 properties are strings that we configure so that the native dialogs (managed by the backend) present titles according to the language selected by the user. The function that acts as the constructor (NewApp) for App simply creates the pointer to the database struct.
Next we see 2 methods required by the options that Wails needs: startup and beforeClose, which we will pass respectively to the OnStartup and OnBeforeClose options. By doing so they will automatically receive a Go Context. beforeClose simply closes the connection to the database when closing the application. But startup does more. First, it sets the context it receives in its corresponding field. Second, it registers a series of event listeners in the backend that we will need to trigger a series of actions.
In Wails all event listeners have this signature:
. ├── 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
That is, it receives the context (the one we save in the ctx field of App), the name of the event (which we will have established in the frontend) and a callback that will execute the action we need, and which in turn can receive optional parameters, of type any or empty interface (interface{}), which is the same, so we will have to make type assertions.
Some of the listeners we declare have nested event emitters declared within them that will be received on the frontend and trigger certain actions there. His signature looks like this:
/* 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()) } }
I'm not going to go into detail about what these listeners do, not just for brevity but also because Go is expressive enough that you can tell what they do just by reading the code. I'll just explain a few of them. The "change_titles" listener expects to receive an event with that name. This event is triggered when the user changes the interface language, changing the title of the application window's title bar to the value received by the listener itself. We use the Wails runtime package to achieve this. The event also receives the titles of the "Select Directory" and "Select File" dialogs which are stored in separate properties of the App struct to be used when needed. As you can see, we need this event because these "native" actions need to be performed from the backend.
Special mention for the listeners "import_data" and "password" which are, so to speak, chained. The first one ("import_data"), when received, triggers the opening of a dialog box with the runtime.OpenFileDialog method. As we can see, this method receives among its options the title to be displayed, which is stored in the selectedFile field of the App struct, as we already explained. If the user selects a file and, therefore, the fileLocation variable is not empty, an event is emitted (called "enter_password") that is received in the frontend to show a popup in which the user is asked to enter the master password that he used when he made the export. When the user does so, the frontend emits an event ("password"), which we receive in our backend listener. The data received (the master password) and the path to the backup file are used by a method of the Db struct, which represents the database (ImportDump). Depending on the result of the execution of said method, a new event ("imported_data") is emitted that will trigger a pop-up window in the frontend with the successful or failed result of the import.
As we can see, Wails events are a powerful and effective way of communication between the backend and the frontend.
The rest of the methods of the App struct are nothing more than the methods that the backend exposes to the frontend, as we already explained, and which are basically the CRUD operations with the database and which, therefore, we explain below.
For this backend part I was inspired (making some modifications) by this post (here on DEV.to) by vikkio88 and his repo of a password manager, that he first created with C#/Avalonia and then adapted to use Go/Fyne (Muscurd-ig).
The "lowest level" part of the backend is the one related to password encryption. The most essential are these 3 functions:
. ├── 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
I won't go into the details of symmetric encryption with AES in Go. Everything you need to know is well explained in this post, here on DEV.to.
AES is a block cipher algorithm that takes a fixed-size key and fixed-size plaintext, and returns fixed-size ciphertext. Because the block size of AES is set to 16 bytes, the plaintext must be at least 16 bytes long. Which causes a problem for us since we want to be able to encrypt/decrypt arbitrary sized data. To solve the problem of minimum plaintext block size the block cipher modes exist. Here I use GCM mode because it is one of the most widely adopted symmetric block cipher modes. GCM requires an IV (initialization vector [array]) which must always be randomly generated (the term used for such an array is nonce).
Basically, the encrypt function takes a plaintext to encrypt and a secret key that will always be 32 bytes long and generates an AES encryptor with that key. With that encryptor we generate a gcm object that we use to create a 12-byte initialization vector (nonce). The Seal method of the gcm object allows us to "join" and encrypt the plaintext (as a slice of bytes) with the vector nonce and finally convert its result back into a string.
The decrypt function does the opposite: the first part of it is equal to encrypt, then since we know that the ciphertext is actually nonce ciphertext, we can split the ciphertext into its 2 components. The NonceSize method of the gcm object always results in "12" (which is the length of the nonce), and thus we split the byte slice at the same time as we decrypt it with the Open method of the gcm object. Finally, we convert the result into a string.
The keyfy function ensures that we have a 32-byte secret key (by padding it with "0" to reach that length). We will see that in the frontend we make sure that the user does not enter characters of more than one byte (non-ASCII characters), so that the result of this function is always 32 bytes long.
The rest of the code in this file is essentially responsible for encoding/decoding to base64 the input/output of the functions described above.
To store all the application data we use cloverDB. It is a lightweight and embedded document-oriented NoSQL Database, similar to MongoDB. One of the features of this database is that when records are saved, they are assigned an ID (by default, the field is designated as _id, a bit like what happens in MongoDB) which is a uuid string (v4). So if we want to sort the records by entry order, we must assign them a timestamp when they are stored.
Based on these facts, we will create our models and their associated methods (master_password.go & password_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 has a private field (clear) that is not stored/retrieved (hence no clover tag) in/from the database, i.e. it only lives in memory and is not stored on disk. This property is the unencrypted master password itself and will be used as an encryption key for password entries. This value is stored by a setter on the MasterPassword object or set (by a callback) as a non-exported (private) field in the struct Db of the package of the same name (db.go). For password inputs we use 2 structs, one that does not have the encrypted password and another one in which the password is already encrypted, which is the object that will actually be stored in the database (similar to a DTO, data transfer object). The encryption/decryption methods of both structs internally use a Crypto object, which has a property with the encryption key (which is the master password converted to a 32-byte long slice):
/* 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 Password has 3 methods that play an important role in data saving/recovery:
/* 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 } ...
GetCrypto allows you to get the current instance of the Crypto object so that the db.go package can encrypt/decrypt password entries. SetClear is the setter we mentioned earlier and Check is the function that verifies if the master password entered by the user is correct; as we can see, in addition to the password, it takes as an argument a callback, which depending on the case will be the aforementioned setter (when we import data from the backup file) or the SetMasterPassword method of the db.go package that sets the value in the private field of the Db struct when the user logs in.
I'm not going to explain in detail all the methods of the db.go package because most of its code is related to the way of working with cloverDB, which you can check in its documentation, although I have already mentioned some important things that will be used here.
. ├── 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
First we have the struct that will store a pointer to the cloverDB instance. It also stores a pointer to a "full" instance of the MasterPassword struct. "Full" here means that it stores both the encrypted master password (meaning it exists in the database and is therefore the current master password) and the unencrypted master password, which will be used for encryption of the password entries. Next we have setupCollections, NewDb, and Close, which are functions and methods to setup the database when the application is started and closed. cloverDB does not automatically create a storage file/directory when instantiated with the Open method, instead we have to create it manually. Finally, GetLanguageCode and SaveLanguageCode are methods to retrieve/save the application language selected by the user. Since the selected language code is a small string ("en" or "es"), for simplicity we don't use a struct to store it: for example, to retrieve the language code from the collection (cloverDB works with "documents" and "collections", similar to MongoDB), we simply pass the key under which it is stored ("code") and make a type assertion.
When the user logs in for the first time, the master password is saved in the database: this value (unencrypted) is set in the clear field of the MasterPassword object, which also stores the already encrypted password, and is saved in the Db struct:
/* 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()) } }
The master password recovery is done twice when starting the application:
In both cases, the RecoverMasterPassword method is called, which only if there is a master password stored will set the instance in the cachedMp field of the Db struct:
/* 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 } ...
Next, there are 2 small but important pieces of code:
EventsOn( ctx context.Context, eventName string, callback func(optionalData ...interface{}), ) func()
Beyond the usual CRUD operations typical of any todoapp, we have other functions or methods to comment on:
. ├── 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 is a helper function that creates a PasswordEntryDTO object from a single document obtained from cloverDB. loadManyPasswordEntryDTO does the same, but from a slice of cloverDB documents, generating a slice of PasswordEntryDTO. Finally, loadManyPasswordEntry does the same as loadManyPasswordEntryDTO but also decrypts documents obtained from cloverDB from an instance of the Crypto object generated by the getCryptoInstance method.
Finally, among the methods not related to CRUD, we have those used in the export/import of data:
/* 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 uses the DbDump struct which will be the object saved in the backup file. It takes as its name the path of the directory selected by the user, a date format and an ad hoc extension. Then we create a DbDump instance with the encrypted master password, the DTOs slice (with its corresponding passwords also encrypted) and the language code saved by the user in the database. This object is finally encoded in binary by the Golang gob package in the file we have created, returning the name of the file to the UI to inform the user of its successful creation.
On the other hand, ImportDump takes as arguments the master password that the UI asks the user for, which is the password in effect when the export was performed, and the path to the backup file. Now, it decrypts the selected file using the DbDump structure and then obtains a MasterPassword instance from the encrypted master password stored in DbDump. In the next step, we verify that the password supplied by the user is correct, while setting the clear field in the MasterPassword instance:
/* 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 } ...
Finally, we get an instance of the Crypto object from the MasterPasword instance created in the previous step and do 2 things in the following loop:
The last thing we have left is to save the language code in the database.
And that's enough for today, this tutorial has already gotten long ??.
In the second part, we will detail the frontend side which, as I said, is made with Svelte.
If you are impatient, as I already told you, you can find all the code in this repo.
See you in the second part. Happy coding ?!!
The above is the detailed content of A minimalist password manager desktop app: a foray into Golangs Wails framework (Part 1). For more information, please follow other related articles on the PHP Chinese website!