Maison >développement back-end >Golang >Une application de bureau de gestion de mots de passe minimaliste : une incursion dans le framework Wails de Golang (Partie 1)

Une application de bureau de gestion de mots de passe minimaliste : une incursion dans le framework Wails de Golang (Partie 1)

Mary-Kate Olsen
Mary-Kate Olsenoriginal
2024-12-20 10:22:10473parcourir

I - Pourquoi développer des applications bureautiques aujourd'hui ?

C'est une question que tous les développeurs se sont posés, surtout s'ils viennent du monde du webdev : "Si je peux exécuter presque tout ce qui sera rendu dans le navigateur et servira à presque tous les objectifs que je veux, qui aurait besoin de télécharger notre application et de l'exécuter sur son ordinateur ?". Mais au-delà de l'exigence évidente du travail que nous effectuons (pour nous-mêmes ou pour une entreprise, par exemple pouvoir utiliser toutes les fonctionnalités du système d'exploitation, de meilleures performances, des capacités hors ligne, une sécurité et une intégration améliorées, etc.), il y a le expérience que nous, en tant que développeurs, acquérons en touchant de nouveaux aspects de la programmation qui nous enrichiront toujours.

Si vous êtes passionné par Golang, comme moi, et que vous avez développé du backend dans ce langage, mais que vous avez aussi fait du frontend avec HTML, CSS et JavaScript (ou certains de ses frameworks) ce post est fait pour vous, car sans avoir besoin pour apprendre une nouvelle technologie, vous êtes plus que capable de créer des applications de bureau.

II - La réponse s'appelle Wails

Il y a de fortes chances que vous connaissiez déjà Electron ou Tauri. Les deux utilisent des technologies Web pour le frontend ; le premier utilise JavaScript (ou plutôt NodeJs) dans son backend, et le second utilise Rust. Mais les deux présentent des inconvénients plus ou moins notables. Les applications Electron ont de très gros binaires (car elles contiennent un navigateur Chromium complet) et consomment beaucoup de mémoire. Les applications Tauri améliorent ces aspects (entre autres parce qu'elles utilisent WebView2 [Windows]/WebKit [macOS & Linux] au lieu de Chromium), mais le binaire est encore relativement volumineux et leurs temps de compilation sont… sont ceux de Rust ? (sans parler de leur courbe d'apprentissage, même si j'adore Rust, je le pense vraiment ?).

En utilisant Wails, vous obtenez le meilleur de tous ces mondes de développement d'applications de bureau avec les technologies Web que je viens de décrire, ainsi que tous les avantages liés à l'utilisation de Go :

  • langage facile à apprendre et extraordinairement expressif,
  • exécution rapide et, surtout, compilation rapide,
  • Compilation croisée « prête à l'emploi »,
  • des petits binaires qui fonctionnent avec une consommation de mémoire modérée (à titre d'exemple, l'application que nous développerons ici ferait ~100 Mo avec Electron et avec d'autres frameworks GUI natifs comme Fyne, ~20 Mo ; avec Wails ce n'est que 4 Mo ? !!),
  • et la possibilité d'utiliser le framework web de votre choix (même Vanilla JS) avec lequel vous obtenez la facilité de concevoir des UI "modernes" qui améliorent l'expérience utilisateur.

Oui, si je voulais utiliser Go pour créer des applications de bureau il existe d'autres possibilités (natives ou non). Je mentionnerais Fyne et go-gtk. Fyne est un framework GUI qui permet de créer facilement des applications natives et bien qu'elles puissent avoir un design élégant, les capacités du framework sont quelque peu limitées ou nécessitent un grand effort de la part du développeur pour obtenir le même résultat que d'autres outils et/ou langages vous permettraient de le faire facilement. Je peux dire la même chose de go-gtk, qui est une liaison Go pour GTK : oui, c'est vrai que vous obtiendrez des applications natives dont les limites seront dans vos propres capacités, mais entrer dans la bibliothèque GTK, c'est comme partir en expédition à travers la jungle ?…

III - Une approche de Wails : Nu-i uita - Gestionnaire de mots de passe minimaliste

Tout d'abord, pour ceux qui se demandent ce que signifie Nu-i uita : en roumain, cela signifie en gros "ne les oubliez pas". Je pensais que c'était un nom original...

Vous pouvez voir l'intégralité du code de l'application dans ce référentiel GitHub. Si vous souhaitez l'essayer tout de suite, vous pouvez télécharger l'exécutable ici (pour Windows et Linux).

A minimalist password manager desktop app: a foray into Golang

Je vais décrire brièvement le fonctionnement de l'application : l'utilisateur se connecte pour la première fois et la fenêtre de connexion lui demande de saisir un mot de passe principal. Ceci est enregistré crypté en utilisant le mot de passe lui-même comme clé de cryptage. Cette fenêtre de connexion mène à une autre interface où l'utilisateur peut lister les mots de passe enregistrés pour les sites Web correspondants et les noms d'utilisateur utilisés (vous pouvez également rechercher dans cette liste par nom d'utilisateur ou site Web). Vous pouvez cliquer sur chaque élément de la liste et voir ses détails, le copier dans le presse-papiers, le modifier ou le supprimer. De plus, lors de l'ajout de nouveaux éléments, vos mots de passe seront cryptés en utilisant le mot de passe principal comme clé. Dans la fenêtre de configuration, vous pouvez choisir la langue de votre choix (actuellement uniquement l'anglais et l'espagnol), supprimer toutes les données stockées, les exporter ou les importer à partir d'un fichier de sauvegarde. Lors de l'importation de données, il sera demandé à l'utilisateur le mot de passe principal utilisé lors de l'exportation, et les données importées seront désormais enregistrées et cryptées avec le mot de passe principal actuel. Par la suite, chaque fois que l'utilisateur se reconnectera à l'application, il sera invité à saisir le mot de passe principal actuel.

Je ne vais pas m'attarder sur les exigences dont vous avez besoin pour utiliser Wails car c'est bien expliqué dans son excellente documentation. Dans tous les cas, il est essentiel d'installer sa puissante CLI (allez installer github.com/wailsapp/wails/v2/cmd/wails@latest), qui permet de générer un échafaudage pour l'application, de recharger à chaud lors de l'édition du code. , et créez des exécutables (y compris la compilation croisée).

La CLI Wails vous permet de générer des projets avec une variété de frameworks frontend, mais pour une raison quelconque, les créateurs de Wails semblent préférer Svelte... car c'est la première option qu'ils mentionnent. Lorsque vous utilisez la commande wails init -n myproject -t svelte-ts, vous générez un projet avec Svelte3 et TypeScript.

Si pour une raison quelconque vous préférez utiliser Svelte5 avec sa nouvelle fonctionnalité système de runes, j'ai créé un script bash qui automatise la génération de projets avec Svelte5. Dans ce cas, vous devrez également installer la Wails CLI.

Les fonctionnalités de l'application que j'ai mentionnées ci-dessus constituent les exigences de toute todoapp (ce qui est toujours un bon moyen d'apprendre quelque chose de nouveau en programmation), mais ici nous ajoutons un plus de fonctionnalités (par exemple à la fois dans le backend, l'utilisation du cryptage symétrique, et dans le frontend, l'utilisation de l'Internationalisation) qui le rendent un peu plus utile et instructif qu'une simple application de tâches.

Ok, assez d'introduction, alors passons aux choses sérieuses ?.

IV - Structure du projet Wails : un aperçu du fonctionnement de ce cadre

Si vous choisissez de créer un projet Wails avec Svelte Typescript avec la CLI en exécutant la commande wails init -n myproject -t svelte-ts (ou avec le script bash que j'ai créé et dont je vous ai déjà parlé auparavant, cela génère Projets Wails avec Svelte5), vous aurez une structure de répertoires très similaire à celle-ci :

.
├── 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

Ce que vous venez de voir est la structure de candidature terminée. La seule différence avec celui généré par Wails CLI est qu'avec lui, vous obtiendrez l'échafaudage d'une application Wails avec une interface Svelte3 TypeScript, et avec mon script, en plus d'avoir Svelte5, Tailwindcss Daisyui est intégré.

Mais voyons comment fonctionne une application Wails en général et en précisant en même temps l'explication de notre cas :

A minimalist password manager desktop app: a foray into Golang

Comme le dit la documentation Wails : "Une application Wails est une application Go standard, avec un webkit frontend. La partie Go de l'application se compose du code de l'application et d'une bibliothèque d'exécution qui fournit un certain nombre d'opérations utiles, comme contrôler la fenêtre de l'application. Le frontend est une fenêtre webkit qui affichera les actifs du frontend". Bref, et comme nous le savons probablement déjà si nous avons créé des applications de bureau avec les technologies web, expliqué très brièvement, l'application se compose d'un backend (dans notre cas écrit en Go) et d'un frontend dont les actifs sont gérés par une fenêtre Webkit (en le cas du système d'exploitation Windows, Webview2), quelque chose comme l'essence d'un serveur/navigateur Web qui sert/restitue les actifs frontend.

L'application principale dans notre cas spécifique dans laquelle nous souhaitons que l'application puisse fonctionner à la fois sous Windows et Linux se compose du code suivant :

/* 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())
    }
}

La première chose que nous devons faire est d'instancier une structure (avec la fonction NewApp), que nous avons convenu d'appeler App, qui doit avoir un champ avec un Go Context. Ensuite, la méthode Run de Wails est celle qui démarre l'application. Nous devons lui transmettre une série d'options. L’une de ces options obligatoires concerne les actifs. Une fois que Wails compile le frontend, il le génère dans le dossier "frontend/dist". En utilisant la directive //go:embed all:frontend/dist (cette fonctionnalité magique de Go), nous pouvons intégrer l'intégralité de notre interface dans l'exécutable final. Dans le cas de Linux, si nous voulons intégrer l'icône de l'application, nous devons également utiliser la directive //go:embed.

Je n'entrerai pas dans le reste des options, que vous pouvez vérifier dans la documentation. Je dirai juste deux choses concernant les options. La première est que le titre qui apparaît dans la barre de titre de l'application peut être défini ici en option, mais dans notre application, où l'utilisateur peut choisir la langue qu'il souhaite, nous les définirons (en utilisant le runtime Wails) lorsque nous recevrons le événement de changement de langue que l'utilisateur peut effectuer. Nous verrons cela plus tard.

Le deuxième problème important lié aux options est l'option Bind. La documentation explique très bien sa signification : "L'option Bind est l'une des options les plus importantes dans une application Wails. Elle spécifie les méthodes de structure à exposer au frontend. Pensez aux structures comme les contrôleurs dans un site Web traditionnel. application." En effet : les méthodes publiques de la structure App, qui sont celles qui exposent le backend au frontend, réalisent la magie de « connecter » Go avec JavaScript. Ces méthodes publiques de ladite structure sont converties en fonctions JavaScript qui renvoient une promesse par la compilation effectuée par Wails.

L'autre forme de communication importante entre le backend et le frontend (que nous utilisons efficacement dans cette application) est les événements. Wails fournit un système d'événements, dans lequel les événements peuvent être émis ou reçus par Go ou JavaScript. Facultativement, les données peuvent être transmises avec les événements. Étudier la manière dont nous utilisons les événements dans notre application nous amènera à analyser la 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

La première chose que nous voyons est la struct App qui a un champ qui stocke un Go Context, nécessaire à Wails, et un pointeur vers la struct Db (liée à la base de données, comme nous le verrons) . Les 2 autres propriétés sont des chaînes que nous configurons pour que les boîtes de dialogue natives (gérées par le backend) présentent des titres selon la langue sélectionnée par l'utilisateur. La fonction qui agit en tant que constructeur (NewApp) pour App crée simplement le pointeur vers la structure de la base de données.

Ensuite, nous voyons 2 méthodes requises par les options dont Wails a besoin : startup et beforeClose, que nous passerons respectivement aux OnStartup et Options OnBeforeClose. Ce faisant, ils recevront automatiquement un Go Context. beforeClose ferme simplement la connexion à la base de données lors de la fermeture de l'application. Mais la startup fait plus. Tout d'abord, il définit le contexte qu'il reçoit dans son champ correspondant. Deuxièmement, il enregistre une série de écouteurs d'événements dans le backend dont nous aurons besoin pour déclencher une série d'actions.

À Wails, tous les auditeurs d'événements ont cette 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

C'est-à-dire qu'il reçoit le contexte (celui que nous enregistrons dans le champ ctx de App), le nom de l'événement (que nous aurons établi dans le frontend) et un rappel qui s'exécutera l'action dont nous avons besoin, et qui à son tour peut recevoir des paramètres optionnels, de type any ou interface vide (interface{}), ce qui est la même, nous devrons donc faire des assertions de type.

Certains des auditeurs que nous déclarons ont des émetteurs d'événements imbriqués déclarés en leur sein qui seront reçus sur le frontend et y déclencheront certaines actions. Sa signature ressemble à ceci :

/* 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())
    }
}

Je ne vais pas entrer dans les détails de ce que font ces auditeurs, non seulement par souci de brièveté mais aussi parce que Go est suffisamment expressif pour que vous puissiez dire ce qu'ils font simplement en lisant le code. Je vais juste en expliquer quelques-uns. L'écouteur "change_titles" s'attend à recevoir un événement portant ce nom. Cet événement est déclenché lorsque l'utilisateur change la langue de l'interface, en changeant le titre de la barre de titre de la fenêtre de l'application par la valeur reçue par l'écouteur lui-même. Nous utilisons le package d'exécution Wails pour y parvenir. L'événement reçoit également les titres des boîtes de dialogue « Sélectionner un répertoire » et « Sélectionner un fichier » qui sont stockés dans des propriétés distinctes de la structure App pour être utilisés en cas de besoin. Comme vous pouvez le voir, nous avons besoin de cet événement car ces actions « natives » doivent être effectuées depuis le backend.

Mention spéciale pour les auditeurs "import_data" et "password" qui sont pour ainsi dire enchaînés. Le premier ("import_data"), une fois reçu, déclenche l'ouverture d'une boîte de dialogue avec la méthode runtime.OpenFileDialog. Comme nous pouvons le voir, cette méthode reçoit parmi ses options le titre à afficher, qui est stocké dans le champ selectedFile de la struct App, comme nous l'avons déjà expliqué. Si l'utilisateur sélectionne un fichier et, par conséquent, la variable fileLocation n'est pas vide, un événement est émis (appelé "enter_password") qui est reçu dans le frontend pour afficher une fenêtre contextuelle dans laquelle l'utilisateur est invité à saisir le mot de passe principal qu'il a choisi. utilisé lors de l'exportation. Lorsque l'utilisateur le fait, le frontend émet un événement (« mot de passe »), que nous recevons dans notre écouteur backend. Les données reçues (le mot de passe maître) et le chemin d'accès au fichier de sauvegarde sont utilisés par une méthode de la structure Db, qui représente la base de données (ImportDump). En fonction du résultat de l'exécution de ladite méthode, un nouvel événement ("imported_data") est émis qui déclenchera une fenêtre pop-up dans le frontend avec le résultat réussi ou échoué de l'importation.

Comme nous pouvons le constater, les événements Wails sont un moyen de communication puissant et efficace entre le backend et le frontend.

Le reste des méthodes de la structure App ne sont rien de plus que les méthodes que le backend expose au frontend, comme nous l'avons déjà expliqué, et qui sont essentiellement les opérations CRUD avec la base de données et qui, par conséquent, nous expliquons ci-dessous.

V - Backend : la plomberie de l'app

Pour cette partie backend je me suis inspiré (en apportant quelques modifications) de ce post (ici sur DEV.to) de vikkio88 et de son repo d'un gestionnaire de mots de passe, qu'il a d'abord créé avec C#/Avalonia puis adapté pour utiliser Go/ Fyne (Muscurd-ig).

La partie "niveau le plus bas" du backend est celle liée au cryptage des mots de passe. Les plus essentielles sont ces 3 fonctions :

.
├── 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

Je n'entrerai pas dans les détails du chiffrement symétrique avec AES dans Go. Tout ce que vous devez savoir est bien expliqué dans cet article, ici sur DEV.to.

AES est un algorithme de chiffrement par blocs qui prend une clé de taille fixe et un texte brut de taille fixe, et renvoie un texte chiffré de taille fixe. Étant donné que la taille de bloc d’AES est définie sur 16 octets, le texte en clair doit faire au moins 16 octets. Ce qui nous pose problème puisque nous voulons pouvoir crypter/déchiffrer des données de taille arbitraire. Pour résoudre le problème de la taille minimale des blocs de texte en clair, il existe des modes de chiffrement par blocs. Ici, j'utilise le mode GCM car c'est l'un des modes de chiffrement par blocs symétriques les plus largement adoptés. GCM nécessite un IV (vecteur d'initialisation [tableau]) qui doit toujours être généré aléatoirement (le terme utilisé pour un tel tableau est nonce).

Fondamentalement, la fonction encrypt prend un texte en clair à chiffrer et une clé secrète qui fera toujours 32 octets de long et génère un chiffreur AES avec cette clé. Avec ce chiffreur, nous générons un objet gcm que nous utilisons pour créer un vecteur d'initialisation de 12 octets (nonce). La méthode Seal de l'objet gcm nous permet de "joindre" et de chiffrer le texte brut (sous forme de tranche d'octets) avec le vecteur nonce et enfin de reconvertir son résultat en chaîne.

La fonction decrypt fait le contraire : la première partie de celle-ci est égale à encrypt, alors puisque nous savons que le texte chiffré est en fait un texte chiffré nonce, nous pouvons diviser le texte chiffré en ses 2 composantes. La méthode NonceSize de l'objet gcm donne toujours "12" (qui est la longueur du nonce), et ainsi on divise la tranche d'octets en même temps qu'on la déchiffre avec la méthode Open de l'objet gcm. Enfin, nous convertissons le résultat en chaîne.

La fonction keyfy garantit que nous disposons d'une clé secrète de 32 octets (en la complétant avec "0" pour atteindre cette longueur). Nous verrons que dans le frontend nous veillons à ce que l'utilisateur ne saisisse pas de caractères de plus d'un octet (caractères non-ASCII), afin que le résultat de cette fonction fasse toujours 32 octets.

Le reste du code de ce fichier est essentiellement responsable de l'encodage/décodage en base64 l'entrée/sortie des fonctions décrites ci-dessus.

Pour stocker toutes les données d'application, nous utilisons cloverDB. Il s'agit d'une base de données NoSQL légère et intégrée orientée document, similaire à MongoDB. L'une des caractéristiques de cette base de données est que lorsque les enregistrements sont enregistrés, ils se voient attribuer un identifiant (par défaut, le champ est désigné comme _id, un peu comme ce qui se passe dans MongoDB) qui est une chaîne uuid ( v4). Donc si nous voulons trier les enregistrements par ordre d'entrée, nous devons leur attribuer un horodatage lorsqu'ils sont stockés.

Sur la base de ces faits, nous créerons nos modèles et leurs méthodes associées (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 a un champ privé (clair) qui n'est pas stocké/récupéré (donc pas de balise trèfle) dans/depuis la base de données, c'est-à-dire qu'il ne vit qu'en mémoire et n'est pas stocké sur le disque. Cette propriété est le mot de passe principal non chiffré lui-même et sera utilisée comme clé de chiffrement pour les entrées de mot de passe. Cette valeur est stockée par un setter sur l'objet MasterPassword ou définie (par un callback) comme champ non exporté (privé) dans la struct Db du package du même nom (db.go). Pour la saisie du mot de passe, nous utilisons 2 structures, une qui n'a pas le mot de passe crypté et une autre dans laquelle le mot de passe est déjà crypté, qui est l'objet qui sera réellement stocké dans la base de données (similaire à un DTO , objet de transfert de données). Les méthodes de cryptage/déchiffrement des deux structures utilisent en interne un objet Crypto, qui possède une propriété avec la clé de cryptage (qui est le mot de passe principal converti en une tranche de 32 octets) :

/* 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())
    }
}

Le mot de passe principal dispose de 3 méthodes qui jouent un rôle important dans la sauvegarde/récupération des données :

/* 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 vous permet d'obtenir l'instance actuelle de l'objet Crypto afin que le package db.go puisse crypter/déchiffrer les entrées de mot de passe. SetClear est le setter que nous avons mentionné plus tôt et Check est la fonction qui vérifie si le mot de passe principal saisi par l'utilisateur est correct ; comme on peut le voir, en plus du mot de passe, il prend comme argument un callback, qui selon les cas sera le setter susmentionné (quand on importe les données du fichier de sauvegarde ) ou la méthode SetMasterPassword du package db.go qui définit la valeur dans le champ privé de la structure Db lorsque l'utilisateur se connecte dans.

Je ne vais pas expliquer en détail toutes les méthodes du package db.go car la majeure partie de son code est liée à la manière de travailler avec cloverDB, que vous pouvez consulter dans sa documentation, bien que j'ai déjà mentionné certaines choses importantes qui seront utilisées ici.

.
├── 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

Nous avons d’abord la structure qui stockera un pointeur vers l’instance de cloverDB. Il stocke également un pointeur vers une instance « complète » de la structure MasterPassword. "Complet" signifie ici qu'il stocke à la fois le mot de passe principal crypté (ce qui signifie qu'il existe dans la base de données et est donc le mot de passe principal actuel) et le mot de passe principal non crypté, qui sera utilisé pour le cryptage des entrées de mot de passe. Ensuite, nous avons setupCollections, NewDb et Close, qui sont des fonctions et des méthodes pour configurer la base de données lorsque l'application est démarrée et fermée. cloverDB ne crée pas automatiquement un fichier/répertoire de stockage lorsqu'il est instancié avec la méthode Open, nous devons plutôt le créer manuellement. Enfin, GetLanguageCode et SaveLanguageCode sont des méthodes pour récupérer/enregistrer la langue de l'application sélectionnée par l'utilisateur. Puisque le code de langue sélectionné est une petite chaîne ("en" ou "es"), par souci de simplicité nous n'utilisons pas de structure pour le stocker : par exemple, pour récupérer le code de langue de la collection (cloverDB fonctionne avec des "documents" et "collections", similaire à MongoDB), on passe simplement la clé sous laquelle il est stocké ("code") et on fait une assertion de type.

Lorsque l'utilisateur se connecte pour la première fois, le mot de passe principal est enregistré dans la base de données : cette valeur (non chiffrée) est définie dans le champ en clair de l'objet MasterPassword, qui stocke également le mot de passe déjà chiffré mot de passe, et est enregistré dans la structure Db :

/* 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())
    }
}

La récupération du mot de passe maître se fait deux fois au démarrage de l'application :

  1. pour vérifier s'il y a un mot de passe principal stocké dans la base de données.
  2. obtenir une instance MasterPassword et pouvoir utiliser sa méthode Check et vérifier que le mot de passe fourni par l'utilisateur est correct.

Dans les deux cas, la méthode RecoverMasterPassword est appelée, qui, seulement si un mot de passe principal est stocké, définira l'instance dans le champ cachedMp de la structure Db :

/* 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
}

...

Ensuite, il y a 2 petits morceaux de code importants :

  1. SetMasterPassword, qui, comme je l'ai mentionné, est utilisé comme rappel à la méthode Check de l'objet MasterPassword et définira l'objet non chiffré mot de passe principal dans le champ cachedMp de la structure Db uniquement si ce champ n'est pas nul.
  2. getCryptoInstance, qui renverra une instance de l'objet Crypto uniquement si cachedMp n'est pas nul. Sinon, cela paniquera l'application : bien qu'en théorie cette situation ne puisse pas se produire si l'utilisateur est authentifié dans l'application, mais pour des raisons de sécurité, nous terminons l'application si cela se produit :
EventsOn(
    ctx context.Context,
    eventName string,
    callback func(optionalData ...interface{}),
) func()

Au-delà des opérations CRUD habituelles typiques de toute todoapp, nous avons d'autres fonctions ou méthodes à commenter :

.
├── 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 est une fonction d'assistance qui crée un objet PasswordEntryDTO à partir d'un seul document obtenu de cloverDB. loadManyPasswordEntryDTO fait la même chose, mais à partir d'une tranche de documents cloverDB, générant une tranche de PasswordEntryDTO. Enfin, loadManyPasswordEntry fait la même chose que loadManyPasswordEntryDTO mais décrypte également les documents obtenus depuis cloverDB à partir d'une instance de l'objet Crypto généré par le getCryptoInstance méthode.

Enfin, parmi les méthodes non liées au CRUD, nous avons celles utilisées dans l'export/import de données :

/* 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 utilise la structure DbDump qui sera l'objet enregistré dans le fichier de sauvegarde. Il prend pour nom le chemin du répertoire sélectionné par l'utilisateur, un format de date et une extension ad hoc. Ensuite nous créons une instance DbDump avec le mot de passe maître chiffré, la tranche DTOs (avec ses mots de passe correspondants également chiffrés) et le code langue enregistré par l'utilisateur dans la base de données. Cet objet est finalement codé en binaire par le package Golang gob dans le fichier que nous avons créé, renvoyant le nom du fichier à l'UI pour informer l'utilisateur de sa création réussie.

D'autre part, ImportDump prend comme arguments le mot de passe principal que l'interface utilisateur demande à l'utilisateur, qui est le mot de passe en vigueur au moment de l'export, et le chemin d'accès au fichier de sauvegarde. Maintenant, il déchiffre le fichier sélectionné à l'aide de la structure DbDump, puis obtient une instance MasterPassword à partir du mot de passe principal crypté stocké dans DbDump. Dans l'étape suivante, nous vérifions que le mot de passe fourni par l'utilisateur est correct, tout en définissant le champ clair dans l'instance MasterPassword :

/* 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
}

...

Enfin, nous obtenons une instance de l'objet Crypto à partir de l'instance MasterPasword créée à l'étape précédente et faisons 2 choses dans la boucle suivante :

  1. Nous déchiffrons les DTO et les convertissons en PasswordEntry avec le mot de passe déjà déchiffré et
  2. Nous insérons les PasswordEntry's dans la base de données qui sera désormais cryptée avec le nouveau mot de passe principal.

La dernière chose qu'il nous reste est de sauvegarder le code de langue dans la base de données.

Et ça suffit pour aujourd'hui, ce tuto est déjà devenu long ?‍?.

Dans la deuxième partie, nous détaillerons le côté frontend qui, comme je l'ai dit, est réalisé avec Svelte.

Si vous êtes impatient, comme je vous l'ai déjà dit, vous pouvez retrouver tout le code dans ce repo.

Rendez-vous dans la deuxième partie. Bon codage ?!!

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn