ホームページ >バックエンド開発 >Golang >シンプルなパスワード マネージャー デスクトップ アプリ: Golang の Wails フレームワークへの進出 (パート 1)

シンプルなパスワード マネージャー デスクトップ アプリ: Golang の Wails フレームワークへの進出 (パート 1)

Mary-Kate Olsen
Mary-Kate Olsenオリジナル
2024-12-20 10:22:10473ブラウズ

I - なぜ今デスクトップ アプリケーションを開発するのでしょうか?

これは、すべての開発者、特に webdev の世界の開発者が自問したことがある質問です。「ブラウザでレンダリングするほぼすべてのものを実行でき、必要なほぼすべての目的を達成できれば、誰が私たちのアプリケーションをダウンロードして自分のコンピュータで実行する必要があるでしょうか?」しかし、私たちが行っている作業の明白な要件 (自分自身または会社のため、たとえば、すべての OS 機能、パフォーマンスの向上、オフライン機能、セキュリティと統合の向上などを利用できることなど) とは別に、開発者としてプログラミングの新しい側面に触れることで得られる経験は、常に私たちを豊かにしてくれます。

もしあなたが私のように Golang に情熱を持っていて、この言語でバックエンドを開発したことがあるが、HTML、CSS、JavaScript (またはそのフレームワークの一部) でフロントエンドも開発したことがあるなら、この投稿はあなたのためです。新しいテクノロジーを学ぶには、デスクトップ アプリケーションを作成できる以上の能力が必要です。

II - 答えは Wails と呼ばれます

おそらく、エレクトロン または タウリ についてはすでにご存知でしょう。どちらもフロントエンドに Web テクノロジーを使用します。 1 つ目はバックエンドで JavaScript (NodeJs) を使用し、2 つ目は Rust を使用します。しかし、どちらにも多かれ少なかれ顕著な欠点があります。 Electron アプリには非常に大きなバイナリがあり (Chromium ブラウザ全体をパッケージ化しているため)、大量のメモリを消費します。 Tauri アプリはこれらの点を改善しています (特に、Chromium の代わりに WebView2 [Windows]/WebKit [macOS & Linux] を使用しているため)、バイナリはまだ比較的大きく、コンパイル時間は… Rust のものですか? (彼らの学習曲線は言うまでもありませんが、私は Rust が大好きですが、本当にそう思っているのでしょうか?)。

Wails を使用すると、先ほど説明した Web テクノロジを使用したデスクトップ アプリケーション開発のすべての世界を最大限に活用できるほか、Go を使用することで得られるすべての利点も得られます。

  • 学びやすく、非常に表現力豊かな言語
  • 実行が速く、何よりもコンパイルが速い
  • 「すぐに使える」クロスコンパイル、
  • 中程度のメモリ消費で実行される小さなバイナリ (例として、ここで開発するアプリケーションは、Electron では約 100 Mb、Fyne などの他のネイティブ GUI フレームワークでは約 20 Mb ですが、Wails ではわずか 4 Mb です? !!)、
  • また、選択した Web フレームワーク (Vanilla JS も含む) を使用できるため、ユーザー エクスペリエンスを向上させる「最新の」 UI を簡単に設計できます。

はい、Go を使用してデスクトップ アプリケーションを作成したい場合は、別の可能性があります (ネイティブかどうか)。 Fynego-gtk についてお話しします。 Fyne は、ネイティブ アプリを簡単に作成できる GUI フレームワークです。ネイティブ アプリは洗練されたデザインですが、フレームワークの機能は多少制限されているか、同じことを実現するには開発者に多大な努力が必要です。他のツールや言語を使用すると、簡単に実行できます。 GTK の Go バインディングである go-gtk についても同じことが言えます。確かに、制限が自分の能力にあるネイティブ アプリケーションが得られるのは事実です。しかし、GTK ライブラリに入るのは、ジャングルを探検するようなものですか?…

III - Wails へのアプローチ: Nu-i uita - ミニマリストのパスワード マネージャー

まず、Nu-i uita が何を意味するのか疑問に思っている人のために、ルーマニア語で「忘れないでください」という意味です。オリジナルの名前だと思ってました…

この GitHub リポジトリでアプリケーションのコード全体を確認できます。すぐに試してみたい場合は、ここから実行可能ファイルをダウンロードできます (Windows および Linux 用)。

A minimalist password manager desktop app: a foray into Golang

アプリケーションがどのように機能するかを簡単に説明します。ユーザーは初めてログインし、ログイン ウィンドウでマスター パスワードの入力を求められます。パスワードそのものを暗号化キーとして使用して暗号化して保存されます。このログイン ウィンドウは別のインターフェイスにつながり、ユーザーは、対応する Web サイトの保存されたパスワードと使用されているユーザー名を一覧表示できます (このリスト内でユーザー名または Web サイトを検索することもできます)。リスト内の各項目をクリックして詳細を確認したり、クリップボードにコピーしたり、編集または削除したりできます。また、新しいアイテムを追加すると、マスターパスワードをキーとして使用してパスワードが暗号化されます。設定ウィンドウでは、選択した言語 (現在は英語とスペイン語のみ) を選択したり、保存されているデータをすべて削除したり、エクスポートしたり、バックアップ ファイルからインポートしたりできます。データをインポートするとき、ユーザーはエクスポートの実行時に使用されたマスター パスワードの入力を求められ、インポートされたデータは現在のマスター パスワードで保存および暗号化されるようになります。その後、ユーザーがアプリケーションに再度ログインするたびに、現在のマスター パスワードを入力するよう求められます。

Wails を使用するために必要な要件については、優れたドキュメントで詳しく説明されているため、ここでは詳しく説明しません。いずれの場合も、強力な CLI をインストールすることが重要です (github.com/wailsapp/wails/v2/cmd/wails@latest をインストールします)。これにより、アプリケーションのスキャフォールディングを生成したり、コード編集時にホットリロードしたりできます。 、実行可能ファイルをビルドします (クロスコンパイルを含む)。

Wails CLI を使用すると、さまざまなフロントエンド フレームワークを使用してプロジェクトを生成できますが、何らかの理由で Wails の作成者は Svelte を好むようです...なぜなら、それが最初に言及されるオプションだからです。コマンド wails init -n myproject -t svelte-ts を使用すると、Svelte3TypeScript を使用してプロジェクトを生成します。

何らかの理由で、新しい ルーン システム 機能を備えた Svelte5 を使用したい場合は、プロジェクトの生成を自動化する bash スクリプト を作成しました。スヴェルテ5。この場合、Wails CLI もインストールする必要があります。

上で述べたアプリケーションの機能は、あらゆる todoapp の要件を構成します (これは、プログラミングで何か新しいことを学ぶのに常に良い方法です) が、ここでは プラス を追加します。もう少し便利にする機能 (例: バックエンドでの対称暗号化の使用とフロントエンドでの 国際化 の使用)単純な todoapp よりも有益です。

はい、前置きはこれくらいにして、本題に入りましょう?

IV - Wails プロジェクトの構造: このフレームワークがどのように機能するかの概要

CLI でコマンド wails init -n myproject -t svelte-ts を実行して (または、以前に説明した私が作成した bash スクリプトを使用して) Svelte Typescript を使用して Wails プロジェクトを作成することを選択した場合、 Svelte5 を使用した Wails プロジェクト) は、次のようなディレクトリ構造になります:

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

今見たのは、完成したアプリケーション構造です。 Wails CLI によって生成されたものとの唯一の違いは、Svelte3 TypeScript フロントエンドを使用して Wails アプリケーションのスキャフォールディングを取得できることです。私のスクリプトでは、Svelte5 に加えて、Tailwindcss Daisyui が統合されました。

しかし、Wails アプリケーションが一般的にどのように動作するのかを見てみましょう。同時に、私たちのケースの説明を具体的に説明してみましょう。

A minimalist password manager desktop app: a foray into Golang

Wails のドキュメントには次のように書かれています: 「Wails アプリケーションは、Webkit フロントエンドを備えた標準の Go アプリケーションです。アプリケーションの Go 部分は、アプリケーション コードと ランタイム ライブラリ で構成されます」これにより、アプリケーション ウィンドウの制御など、多くの便利な操作が提供されます。フロントエンドは、フロントエンド アセットを表示する webkit ウィンドウです。」つまり、Web テクノロジを使用してデスクトップ アプリケーションを作成したことがある方ならすでにご存知かと思いますが、非常に簡単に説明すると、アプリケーションはバックエンド (この場合は Go で記述) と、そのアセットが Webkit ウィンドウ (Webkit ウィンドウで管理される) フロントエンドで構成されます。 Windows OS の場合、Webview2)、フロントエンド アセットを提供/レンダリングする Web サーバー/ブラウザの本質のようなもの。

アプリケーションを Windows と Linux の両方で実行できるようにする特定のケースのメイン アプリケーションは、次のコードで構成されます。

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

最初に行う必要があるのは、(NewApp 関数を使用して) 構造体をインスタンス化することです。これを App と呼ぶことに同意しました。これには Go Context を含むフィールドが必要です。次に、wails の Run メソッドがアプリケーションを起動します。一連のオプションを渡す必要があります。これらの必須オプションの 1 つはアセットです。 Wails がフロントエンドをコンパイルすると、それが「frontend/dist」フォルダーに生成されます。 //go:embed all:frontend/dist ディレクティブ (Go の魔法の機能) を使用すると、フロントエンド全体を最終的な実行可能ファイルに埋め込むことができます。 Linux の場合、アプリケーション アイコンを埋め込みたい場合は、//go:embed ディレクティブも使用する必要があります。

残りのオプションについては説明しません。ドキュメントで確認してください。オプションに関連して 2 つのことだけ言っておきます。 1 つ目は、アプリケーションのタイトル バーに表示されるタイトルをここでオプションとして設定できることですが、このアプリケーションではユーザーが希望する言語を選択できるため、メッセージを受け取ったときに (Wails ランタイムを使用して) 設定します。ユーザーが行う可能性のある言語変更イベント。これについては後で説明します。

オプションに関連する 2 番目の重要な問題は、Bind オプションです。ドキュメントではその意味が非常によく説明されています: 「Bind オプションは、Wails アプリケーションで最も重要なオプションの 1 つです。フロントエンドに公開する構造体メソッドを指定します。従来の Web における コントローラー のような構造体を考えてください。」応用。"実際、バックエンドをフロントエンドに公開するアプリ構造のパブリック メソッドは、Go を JavaScript で「接続」する魔法を実行します。前記構造体のパブリック メソッドは、Wails によって実行されるコンパイルによって Promise を返す JavaScript 関数に変換されます。

バックエンドとフロントエンド間の通信のもう 1 つの重要な形式 (このアプリケーションで効果的に使用します) は イベント です。 Wails は、Go または JavaScript によってイベントを送受信できる イベント システム を提供します。オプションで、データをイベントとともに渡すことができます。アプリケーションでイベントを使用する方法を研究すると、構造体アプリを分析することができます。

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

最初に表示されるのは、Wails に必要な Go Context を格納するフィールドと、構造体 Db (後で説明するようにデータベースに関連する) へのポインターを含む struct App です。 。他の 2 つのプロパティは、ユーザーが選択した言語に従ってネイティブ ダイアログ (バックエンドによって管理される) がタイトルを表示するように構成する文字列です。 App のコンストラクター (NewApp) として機能する関数は、単にデータベース構造体へのポインターを作成します。

次に、Wails が必要とするオプションに必要な 2 つのメソッドが表示されます: startupbeforeClose。これらをそれぞれ OnStartup に渡します。 OnBeforeClose オプション。そうすることで、自動的に Go Context を受け取ります。 beforeClose は、アプリケーションを閉じるときにデータベースへの接続を単に閉じるだけです。しかし、startup はそれ以上のことを行います。まず、受信したコンテキストを対応するフィールドに設定します。次に、一連のアクションをトリガーするために必要な一連のイベント リスナーをバックエンドに登録します。

Wails では、すべてのイベント リスナーが次の署名を持ちます:

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

つまり、コンテキスト (App の ctx フィールドに保存したもの)、イベントの名前 (フロントエンドで確立したもの)、および実行するコールバックを受け取ります。必要なアクション。また、任意の型または空のインターフェース (interface{}) 型のオプションのパラメーターを受け取ることができます。これは同じであるため、型のアサーションを行う必要があります。

宣言したリスナーの一部には、その中で宣言されたネストされたイベント エミッターがあり、フロントエンドで受信され、そこで特定のアクションがトリガーされます。彼の署名は次のようになります:

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

これらのリスナーが何をするかについては詳しく説明しません。簡潔にするためだけでなく、Go はコードを読むだけで何をするかわかるほど表現力が豊かだからです。そのうちのいくつかを説明します。 「change_titles」リスナーは、その名前のイベントを受信することを期待します。このイベントは、ユーザーがインターフェイス言語を変更し、アプリケーション ウィンドウのタイトル バーのタイトルをリスナー自体が受け取った値に変更するとトリガーされます。これを実現するために、Wails ランタイム パッケージを使用します。このイベントは、必要なときに使用されるアプリ構造体の別のプロパティに保存される「ディレクトリの選択」ダイアログと「ファイルの選択」ダイアログのタイトルも受け取ります。ご覧のとおり、これらの「ネイティブ」アクションはバックエンドから実行する必要があるため、このイベントが必要です。

リスナー「import_data」と「password」については、いわば連鎖であることに注意してください。最初のデータ (「import_data」) を受信すると、 runtime.OpenFileDialog メソッドを使用してダイアログ ボックスを開きます。ご覧のとおり、このメソッドはオプションの中で表示されるタイトルを受け取ります。これは、すでに説明したように、App 構造体の selectedFile フィールドに格納されています。ユーザーがファイルを選択し、したがって fileLocation 変数が空でない場合、イベント (「enter_password」と呼ばれる) が発行され、フロントエンドで受信されて、ユーザーがマスター パスワードの入力を求めるポップアップが表示されます。輸出するときに使用されました。ユーザーがこれを行うと、フロントエンドはイベント (「パスワード」) を発行し、バックエンド リスナーでそれを受け取ります。受信したデータ (マスター パスワード) とバックアップ ファイルへのパスは、データベースを表す Db 構造体のメソッド (ImportDump) によって使用されます。上記メソッドの実行結果に応じて、新しいイベント (「imported_data」) が発行され、フロントエンドでポップアップ ウィンドウがトリガーされ、インポートの成功または失敗の結果が表示されます。

ご覧のとおり、Wails イベントはバックエンドとフロントエンド間の強力かつ効果的な通信方法です。

App 構造体の残りのメソッドは、すでに説明したように、バックエンドがフロントエンドに公開するメソッドにすぎず、基本的にはデータベースに対する CRUD 操作です。したがって、以下で説明します。

V - バックエンド: アプリの配管

このバックエンド部分については、vikkio88 によるこの投稿 (ここ DEV.to) と彼のパスワード マネージャーのリポジトリからインスピレーションを受けて (いくつかの修正を加えて)、彼は最初に C#/Avalonia で作成し、その後 Go/ を使用するように適応させました。ファイン (マスカードイグ)。

バックエンドの「最低レベル」の部分は、パスワード暗号化に関連する部分です。最も重要なのは次の 3 つの関数です:

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

Go の AES を使用した対称暗号化の詳細については説明しません。知っておくべきことはすべて、DEV.to のこの投稿で詳しく説明されています。

AES は、固定サイズの鍵と固定サイズの平文を受け取り、固定サイズの暗号文を返すブロック暗号アルゴリズムです。 AES のブロック サイズは 16 バイトに設定されているため、平文の長さは少なくとも 16 バイトである必要があります。私たちは任意のサイズのデータ​​を暗号化/復号化できるようにしたいので、これが問題を引き起こします。最小平文ブロック サイズの問題を解決するために、ブロック暗号モードが存在します。ここでは、最も広く採用されている対称ブロック暗号モードの 1 つである GCM モードを使用します。 GCM には、常にランダムに生成される必要がある IV (初期化ベクトル [配列]) が必要です (このような配列に使用される用語は nonce です)。

基本的に、encrypt 関数は、暗号化する平文と常に 32 バイト長の秘密鍵を受け取り、その鍵を使用して AES 暗号化子を生成します。その暗号化装置を使用して、12 バイトの初期化ベクトル (nonce) の作成に使用する gcm オブジェクトを生成します。 gcm オブジェクトの Seal メソッドを使用すると、平文 (バイトのスライスとして) をベクトル nonce で「結合」して暗号化し、最後にその結果を文字列に変換できます。

decrypt 関数はその逆を行います。関数の最初の部分は encrypt に等しいため、暗号文が実際には nonce 暗号文であることがわかっているため、暗号文を次のように分割できます。その2つのコンポーネント。 gcm オブジェクトの NonceSize メソッドの結果は常に "12" (ノンスの長さ) になるため、復号化と同時にバイト スライスを分割します。 gcm オブジェクトの Open メソッドを使用します。最後に、結果を文字列に変換します。

keyfy 関数は、32 バイトの秘密鍵を確保します (その長さに達するまで「0」を埋め込むことで)。フロントエンドでは、ユーザーが 1 バイトを超える文字 (非 ASCII 文字) を入力しないようにするため、この関数の結果の長さは常に 32 バイトになることがわかります。

このファイルの残りのコードは基本的に、上記の関数の入出力を base64 にエンコード/デコードする役割を果たします。

すべてのアプリケーション データを保存するには、cloverDB を使用します。これは、MongoDB に似た、軽量で埋め込みドキュメント指向の NoSQL データベース です。このデータベースの特徴の 1 つは、レコードが保存されるときに、uuid 文字列である ID が割り当てられることです (デフォルトでは、フィールドは _id として指定され、MongoDB で行われるのと少し似ています)。 v4)。したがって、エントリ順でレコードを並べ替えたい場合は、レコードを保存するときにタイムスタンプを割り当てる必要があります。

これらの事実に基づいて、モデルとそれに関連するメソッド (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 には、データベースに保存/データベースから取得されない (したがってクローバー タグがない) プライベート フィールド (クリア) があります。つまり、メモリ内にのみ存在し、ディスクには保存されません。このプロパティは暗号化されていないマスター パスワードそのものであり、パスワード エントリの暗号化キーとして使用されます。この値は、MasterPassword オブジェクトの setter によって保存されるか、同じ名前のパッケージの構造体 DB 内の非エクスポート (プライベート) フィールドとして (コールバック によって) 設定されます。 (db.go)。パスワード入力には 2 つの構造体を使用します。1 つは暗号化されたパスワードを持たず、もう 1 つはパスワードがすでに暗号化されており、実際にデータベースに保存されるオブジェクトです (DTO と同様) 、データ転送オブジェクト)。両方の構造体の暗号化/復号化メソッドは、内部で Crypto オブジェクトを使用します。このオブジェクトには、暗号化キー (32 バイト長のスライスに変換されたマスター パスワード) を含むプロパティがあります。

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

マスターパスワードには、データの保存/回復において重要な役割を果たす 3 つの方法があります:

/* 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 を使用すると、db.go パッケージがパスワード エントリを暗号化/復号化できるように、Crypto オブジェクトの現在のインスタンスを取得できます。 SetClear は前述の setter で、Check はユーザーが入力したマスター パスワードが正しいかどうかを検証する関数です。ご覧のとおり、パスワードに加えて、引数として callback を取ります。これは、場合によっては前述の setter になります (バックアップ ファイルからデータをインポートする場合)。 ) または、ユーザーがログインするときに DB 構造体のプライベート フィールドに値を設定する db.go パッケージの SetMasterPassword メソッドで。

db.go パッケージのすべてのメソッドについて詳しく説明するつもりはありません。そのコードのほとんどは cloverDB の操作方法に関連しているためです。これについてはドキュメントで確認できます。ただし、ここで使用するいくつかの重要なことについてはすでに述べました。

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

まず、cloverDB インスタンスへのポインターを格納する構造体があります。また、MasterPassword 構造体の「完全な」インスタンスへのポインターも保存されます。ここでの「完全」とは、暗号化されたマスター パスワード (データベース内に存在するため、現在のマスター パスワードであることを意味します) と、パスワード エントリの暗号化に使用される暗号化されていないマスター パスワードの両方が保存されていることを意味します。次に、setupCollectionsNewDb、および Close があります。これらは、アプリケーションの起動時と終了時にデータベースをセットアップする関数とメソッドです。 cloverDB は、Open メソッドでインスタンス化するときにストレージ ファイル/ディレクトリを自動的に作成しません。代わりに手動で作成する必要があります。最後に、GetLanguageCodeSaveLanguageCode は、ユーザーが選択したアプリケーション言語を取得/保存するメソッドです。選択された言語コードは小さな文字列 (「en」または「es」) であるため、簡単にするために、言語コードをコレクションから取得するために構造体を使用しません (cloverDB は「ドキュメント」で動作します)。 MongoDB と同様の「コレクション」)、保存されているキー (「コード」) を渡して、 タイプを作成するだけです。アサーション.

ユーザーが初めてログインすると、マスター パスワードがデータベースに保存されます。この値 (暗号化されていない) は、MasterPassword オブジェクトのクリア フィールドに設定され、すでに暗号化されたパスワードも保存されます。パスワードであり、データベース構造体に保存されます:

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

アプリケーションの起動時にマスターパスワードの回復が 2 回行われます:

  1. データベースにマスターパスワードが保存されているかどうかを確認します。
  2. MasterPassword インスタンスを取得し、その Check メソッドを使用して、ユーザーが指定したパスワードが正しいことを確認できるようにします。

どちらの場合も、RecoverMasterPassword メソッドが呼び出されます。このメソッドは、マスター パスワードが保存されている場合にのみ、Db 構造体のcachedMp フィールドにインスタンスを設定します。

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

...

次に、小さいながらも重要なコードが 2 つあります:

  1. SetMasterPassword は、前述したように、MasterPassword オブジェクトの Check メソッドへの コールバック として使用され、暗号化されていないパスワードを設定します。 Db 構造体のcachedMp フィールドのマスターパスワードは、そのフィールドが nil でない場合にのみ使用されます。
  2. getCryptoInstance。cachedMp が nil でない場合にのみ、Crypto オブジェクトのインスタンスを返します。そうしないと、アプリケーションがパニックになります。理論的には、ユーザーがアプリケーションで認証されている場合、この状況は発生しませんが、セキュリティ上の理由から、このような状況が発生した場合はアプリケーションを終了します。
EventsOn(
    ctx context.Context,
    eventName string,
    callback func(optionalData ...interface{}),
) func()

todoapp に典型的な通常の CRUD 操作以外にも、コメントできる他の関数やメソッドがあります。

.
├── 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 は、cloverDB から取得した単一のドキュメントから PasswordEntryDTO オブジェクトを作成するヘルパー関数です。 loadManyPasswordEntryDTO も同じことを行いますが、cloverDB ドキュメントのスライスから PasswordEntryDTO のスライスを生成します。最後に、loadManyPasswordEntryloadManyPasswordEntryDTO と同じことを行いますが、getCryptoInstance によって生成された Crypto オブジェクトのインスタンスから cloverDB から取得したドキュメントも復号化します。メソッド。

最後に、CRUD に関係しないメソッドのうち、データのエクスポート/インポートで使用されるメソッドがあります。

/* 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 は、バックアップ ファイルに保存されるオブジェクトとなる DbDump 構造体を使用します。これは、ユーザーが選択したディレクトリのパス、日付形式、および アドホック 拡張子を名前として受け取ります。次に、暗号化されたマスター パスワード、DTO スライス (対応するパスワードも暗号化されている)、およびユーザーがデータベースに保存した言語コードを使用して DbDump インスタンスを作成します。このオブジェクトは、作成したファイル内の Golang gob パッケージによって最終的にバイナリでエンコードされ、ファイルの名前が UI に返されて、作成が成功したことがユーザーに通知されます。

一方、ImportDump は、UI がユーザーに要求するマスター パスワード (エクスポート実行時に有効なパスワード) とバックアップ ファイルへのパスを引数として受け取ります。ここで、DbDump 構造を使用して選択したファイルを復号化し、DbDump に保存されている暗号化されたマスター パスワードから MasterPassword インスタンスを取得します。次のステップでは、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
}

...

最後に、前のステップで作成した MasterPasword インスタンスから Crypto オブジェクトのインスタンスを取得し、次のループで 2 つのことを実行します。

  1. DTO を復号化し、既に復号化されたパスワードを使用して PasswordEntry
  2. に変換します。
  3. PasswordEntry のをデータベースに挿入します。これで、新しいマスター パスワードで暗号化されます。

最後に残っているのは、言語コードをデータベースに保存することです。

今日はこれで十分です。このチュートリアルはすでに長くなりました ?‍?.

第 2 部では、前述したように Svelte で作られたフロントエンド側について詳しく説明します。

すでにお伝えしたように、せっかちな方は、このリポジトリですべてのコードを見つけることができます。

第二部でお会いしましょう。コーディングを楽しんでください?!!

以上がシンプルなパスワード マネージャー デスクトップ アプリ: Golang の Wails フレームワークへの進出 (パート 1)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。