ok
b60328b4bc38468d94db34d3b9f75772595f0a3c
10 files changed
WireGuardConfigGenerator/DataModel/Root.csWireGuardConfigGenerator/Form1.Designer.csWireGuardConfigGenerator/Form1.csWireGuardConfigGenerator/FormPassword.Designer.csWireGuardConfigGenerator/FormPassword.csWireGuardConfigGenerator/FormPassword.resxWireGuardConfigGenerator/Helpers/CryptoUtils.csWireGuardConfigGenerator/Helpers/TreeHelpers.csWireGuardConfigGenerator/UserControlTree.Designer.csWireGuardConfigGenerator/UserControlTree.cs
diff --git a/WireGuardConfigGenerator/DataModel/Root.cs b/WireGuardConfigGenerator/DataModel/Root.cs
index 0c04f59..c5291b5 100644
--- a/WireGuardConfigGenerator/DataModel/Root.cs
+++ b/WireGuardConfigGenerator/DataModel/Root.cs
@@ -1,5 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
+using WireGuardConfigGenerator.Helpers;
namespace WireGuardConfigGenerator.DataModel;
@@ -14,21 +15,17 @@ public class Root
public List<Group> Groups { get; set; } = [];
- public void Save()
- {
- var json =JsonSerializer.Serialize(this, options);
- File.WriteAllText("root.json", json);
- }
+ public async Task SaveAsync(string path, string password) =>
+ await CryptoUtils.CompressAndEncryptToFileAsync(this, password, path);
- public void Load()
+ public async Task LoadAsync(string path, string password)
{
- if (File.Exists("root.json"))
+ if (File.Exists(path))
{
- var json = File.ReadAllText("root.json");
- var obj = JsonSerializer.Deserialize<Root>(json);
- if (obj != null)
+ var root = await CryptoUtils.DecryptAndDecompressFromFileAsync<Root>(path, password) ?? new();
+ if (root != null)
{
- Groups = obj.Groups;
+ Groups = root.Groups;
}
}
}
diff --git a/WireGuardConfigGenerator/Form1.Designer.cs b/WireGuardConfigGenerator/Form1.Designer.cs
index 834a6da..1d064bf 100644
--- a/WireGuardConfigGenerator/Form1.Designer.cs
+++ b/WireGuardConfigGenerator/Form1.Designer.cs
@@ -30,6 +30,8 @@
{
menuStrip1 = new MenuStrip();
fileToolStripMenuItem = new ToolStripMenuItem();
+ toolStripMenuItem1 = new ToolStripMenuItem();
+ toolStripSeparator1 = new ToolStripSeparator();
exitToolStripMenuItem = new ToolStripMenuItem();
statusStrip1 = new StatusStrip();
splitContainer1 = new SplitContainer();
@@ -51,15 +53,27 @@
//
// fileToolStripMenuItem
//
- fileToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { exitToolStripMenuItem });
+ fileToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem1, toolStripSeparator1, exitToolStripMenuItem });
fileToolStripMenuItem.Name = "fileToolStripMenuItem";
fileToolStripMenuItem.Size = new Size(37, 20);
fileToolStripMenuItem.Text = "File";
//
+ // toolStripMenuItem1
+ //
+ toolStripMenuItem1.Name = "toolStripMenuItem1";
+ toolStripMenuItem1.Size = new Size(180, 22);
+ toolStripMenuItem1.Text = "Open ...";
+ toolStripMenuItem1.Click += OpenFile_Click;
+ //
+ // toolStripSeparator1
+ //
+ toolStripSeparator1.Name = "toolStripSeparator1";
+ toolStripSeparator1.Size = new Size(177, 6);
+ //
// exitToolStripMenuItem
//
exitToolStripMenuItem.Name = "exitToolStripMenuItem";
- exitToolStripMenuItem.Size = new Size(93, 22);
+ exitToolStripMenuItem.Size = new Size(180, 22);
exitToolStripMenuItem.Text = "Exit";
exitToolStripMenuItem.Click += Exit_Click;
//
@@ -120,5 +134,7 @@
private StatusStrip statusStrip1;
private SplitContainer splitContainer1;
private UserControlTree userControlTree1;
+ private ToolStripMenuItem toolStripMenuItem1;
+ private ToolStripSeparator toolStripSeparator1;
}
}
diff --git a/WireGuardConfigGenerator/Form1.cs b/WireGuardConfigGenerator/Form1.cs
index 0460222..b04ea24 100644
--- a/WireGuardConfigGenerator/Form1.cs
+++ b/WireGuardConfigGenerator/Form1.cs
@@ -14,4 +14,11 @@ public partial class Form1 : Form
this.Close();
}
+ private async void OpenFile_Click(object sender, EventArgs e)
+ {
+ var f = new FormPassword();
+
+ if( f.ShowDialog(this) == DialogResult.OK)
+ await this.userControlTree1.LoadAsync(f.Path, f.Password);
+ }
}
diff --git a/WireGuardConfigGenerator/FormPassword.Designer.cs b/WireGuardConfigGenerator/FormPassword.Designer.cs
new file mode 100644
index 0000000..58b2c46
--- /dev/null
+++ b/WireGuardConfigGenerator/FormPassword.Designer.cs
@@ -0,0 +1,126 @@
+namespace WireGuardConfigGenerator
+{
+ partial class FormPassword
+ {
+ /// <summary>
+ /// Required designer variable.
+ /// </summary>
+ private System.ComponentModel.IContainer components = null;
+
+ /// <summary>
+ /// Clean up any resources being used.
+ /// </summary>
+ /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ /// <summary>
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ /// </summary>
+ private void InitializeComponent()
+ {
+ label1 = new Label();
+ txtPassword = new TextBox();
+ btnOk = new Button();
+ btnCancel = new Button();
+ txtPath = new TextBox();
+ label2 = new Label();
+ SuspendLayout();
+ //
+ // label1
+ //
+ label1.AutoSize = true;
+ label1.Location = new Point(16, 47);
+ label1.Name = "label1";
+ label1.Size = new Size(57, 15);
+ label1.TabIndex = 0;
+ label1.Text = "password";
+ //
+ // txtPassword
+ //
+ txtPassword.Location = new Point(16, 65);
+ txtPassword.Name = "txtPassword";
+ txtPassword.PasswordChar = '*';
+ txtPassword.Size = new Size(167, 23);
+ txtPassword.TabIndex = 1;
+ //
+ // btnOk
+ //
+ btnOk.Location = new Point(135, 94);
+ btnOk.Name = "btnOk";
+ btnOk.Size = new Size(48, 23);
+ btnOk.TabIndex = 2;
+ btnOk.Text = "Ok";
+ btnOk.UseVisualStyleBackColor = true;
+ btnOk.Click += Ok_Click;
+ //
+ // btnCancel
+ //
+ btnCancel.Location = new Point(54, 94);
+ btnCancel.Name = "btnCancel";
+ btnCancel.Size = new Size(75, 23);
+ btnCancel.TabIndex = 3;
+ btnCancel.Text = "Cancel";
+ btnCancel.UseVisualStyleBackColor = true;
+ btnCancel.Click += Cancel_Click;
+ //
+ // txtPath
+ //
+ txtPath.Location = new Point(16, 21);
+ txtPath.Name = "txtPath";
+ txtPath.Size = new Size(167, 23);
+ txtPath.TabIndex = 5;
+ txtPath.Text = "wireguard.db";
+ //
+ // label2
+ //
+ label2.AutoSize = true;
+ label2.Location = new Point(16, 3);
+ label2.Name = "label2";
+ label2.Size = new Size(23, 15);
+ label2.TabIndex = 4;
+ label2.Text = "file";
+ //
+ // FormPassword
+ //
+ AcceptButton = btnOk;
+ AutoScaleDimensions = new SizeF(7F, 15F);
+ AutoScaleMode = AutoScaleMode.Font;
+ CancelButton = btnCancel;
+ ClientSize = new Size(193, 126);
+ Controls.Add(txtPath);
+ Controls.Add(label2);
+ Controls.Add(btnCancel);
+ Controls.Add(btnOk);
+ Controls.Add(txtPassword);
+ Controls.Add(label1);
+ FormBorderStyle = FormBorderStyle.Fixed3D;
+ MaximizeBox = false;
+ MinimizeBox = false;
+ Name = "FormPassword";
+ ShowInTaskbar = false;
+ StartPosition = FormStartPosition.CenterParent;
+ Text = "FormPassword";
+ ResumeLayout(false);
+ PerformLayout();
+ }
+
+ #endregion
+
+ private Label label1;
+ private TextBox txtPassword;
+ private Button btnOk;
+ private Button btnCancel;
+ private TextBox txtPath;
+ private Label label2;
+ }
+}
\ No newline at end of file
diff --git a/WireGuardConfigGenerator/FormPassword.cs b/WireGuardConfigGenerator/FormPassword.cs
new file mode 100644
index 0000000..9b31927
--- /dev/null
+++ b/WireGuardConfigGenerator/FormPassword.cs
@@ -0,0 +1,24 @@
+namespace WireGuardConfigGenerator;
+
+public partial class FormPassword : Form
+{
+ public FormPassword()
+ {
+ InitializeComponent();
+ }
+
+ public string Password => this.txtPassword.Text.Trim();
+
+ public string Path => this.txtPath.Text.Trim();
+
+ private void Ok_Click(object sender, EventArgs e)
+ {
+ this.DialogResult = DialogResult.OK;
+ this.Close();
+ }
+
+ private void Cancel_Click(object sender, EventArgs e)
+ {
+ this.Close();
+ }
+}
diff --git a/WireGuardConfigGenerator/FormPassword.resx b/WireGuardConfigGenerator/FormPassword.resx
new file mode 100644
index 0000000..8b2ff64
--- /dev/null
+++ b/WireGuardConfigGenerator/FormPassword.resx
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+</root>
\ No newline at end of file
diff --git a/WireGuardConfigGenerator/Helpers/CryptoUtils.cs b/WireGuardConfigGenerator/Helpers/CryptoUtils.cs
new file mode 100644
index 0000000..fad0be2
--- /dev/null
+++ b/WireGuardConfigGenerator/Helpers/CryptoUtils.cs
@@ -0,0 +1,126 @@
+using System.IO.Compression;
+using System.Security.Cryptography;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace WireGuardConfigGenerator.Helpers;
+
+public static class CryptoUtils
+{
+ private const int SaltSize = 16; // 128 bits alt
+ private const int KeySize = 32; // 256 bits AES-256
+ private const int IVSize = 16; // 128 bits AES IV
+ private const int Iterations = 100000; // PBKDF2 iterations
+
+ public static async Task<bool> CompressAndEncryptToFileAsync<T>(T model, string password, string filePath, JsonSerializerOptions? options = null, CancellationToken ct = default)
+ {
+ options ??= new JsonSerializerOptions
+ {
+ WriteIndented = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
+ };
+
+ // 1. to JSON
+ string json = JsonSerializer.Serialize(model, options);
+
+ // 2. Compressing using GZIP
+ byte[] compressed;
+ using (var outputStream = new MemoryStream())
+ {
+ using (var gzipStream = new GZipStream(outputStream, CompressionLevel.Optimal))
+ using (var writer = new StreamWriter(gzipStream))
+ {
+ await writer.WriteAsync(json);
+ }
+ compressed = outputStream.ToArray();
+ }
+
+ // 3. Make some salt
+ byte[] salt = RandomNumberGenerator.GetBytes(SaltSize);
+
+ // 4. Key and IV derive using PBKDF2
+ using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, Iterations, HashAlgorithmName.SHA256);
+ byte[] keyMaterial = pbkdf2.GetBytes(KeySize + IVSize);
+ byte[] key = new byte[KeySize];
+ byte[] iv = new byte[IVSize];
+ Array.Copy(keyMaterial, 0, key, 0, KeySize);
+ Array.Copy(keyMaterial, KeySize, iv, 0, IVSize);
+
+ // 5. Encrypt using AES
+ byte[] encrypted;
+ using (var aes = Aes.Create())
+ {
+ aes.Key = key;
+ aes.IV = iv;
+ using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
+ using var ms = new MemoryStream();
+ using (var cryptostream = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
+ {
+ await cryptostream.WriteAsync(compressed, ct);
+ }
+ encrypted = ms.ToArray();
+ }
+
+ // 6. Salt prefixed to ciphertext
+ byte[] result = new byte[SaltSize + encrypted.Length];
+ Array.Copy(salt, 0, result, 0, SaltSize);
+ Array.Copy(encrypted, 0, result, SaltSize, encrypted.Length);
+
+ // 7. Async file write
+ await File.WriteAllBytesAsync(filePath, result, ct);
+
+ return true;
+ }
+
+ public static async Task<T?> DecryptAndDecompressFromFileAsync<T>(string filePath, string password, JsonSerializerOptions? options = null, CancellationToken ct = default)
+ {
+ options ??= new JsonSerializerOptions();
+
+ // 1. Asynch read file
+ byte[] encryptedData = await File.ReadAllBytesAsync(filePath, ct);
+
+ // 2. Salt and ciphertext
+ if (encryptedData.Length < SaltSize)
+ throw new ArgumentException("Encrypted data is too short to contain salt.");
+
+ byte[] salt = new byte[SaltSize];
+ byte[] ciphertext = new byte[encryptedData.Length - SaltSize];
+ Array.Copy(encryptedData, 0, salt, 0, SaltSize);
+ Array.Copy(encryptedData, SaltSize, ciphertext, 0, ciphertext.Length);
+
+ // 3. Key and IV using PBKDF2
+ using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, Iterations, HashAlgorithmName.SHA256);
+ byte[] keyMaterial = pbkdf2.GetBytes(KeySize + IVSize);
+ byte[] key = new byte[KeySize];
+ byte[] iv = new byte[IVSize];
+ Array.Copy(keyMaterial, 0, key, 0, KeySize);
+ Array.Copy(keyMaterial, KeySize, iv, 0, IVSize);
+
+ // 4. Decrypten using AES
+ byte[] decompressed;
+ using (var aes = Aes.Create())
+ {
+ aes.Key = key;
+ aes.IV = iv;
+ using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
+ using var ms = new MemoryStream();
+ using (var cryptostream = new CryptoStream(ms, decryptor, CryptoStreamMode.Write))
+ {
+ await cryptostream.WriteAsync(ciphertext, ct);
+ }
+ decompressed = ms.ToArray();
+ }
+
+ // 5. Decompress using GZIP
+ string json;
+ using (var inputStream = new MemoryStream(decompressed))
+ using (var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress))
+ using (var reader = new StreamReader(gzipStream))
+ {
+ json = await reader.ReadToEndAsync(ct);
+ }
+
+ // 6. Deserialise to model
+ return JsonSerializer.Deserialize<T>(json, options);
+ }
+}
diff --git a/WireGuardConfigGenerator/Helpers/TreeHelpers.cs b/WireGuardConfigGenerator/Helpers/TreeHelpers.cs
index 4a01a20..ab32540 100644
--- a/WireGuardConfigGenerator/Helpers/TreeHelpers.cs
+++ b/WireGuardConfigGenerator/Helpers/TreeHelpers.cs
@@ -4,9 +4,9 @@ namespace WireGuardConfigGenerator.Helpers;
public static class TreeHelpers
{
- public static void LoadTree(TreeView treeView, Root root)
+ public async static Task LoadTreeAsync(TreeView treeView, Root root, string path, string password)
{
- root.Load();
+ await root.LoadAsync(path, password);
treeView.Nodes.Clear();
foreach (var group in root.Groups)
diff --git a/WireGuardConfigGenerator/UserControlTree.Designer.cs b/WireGuardConfigGenerator/UserControlTree.Designer.cs
index 69a22a5..f81073c 100644
--- a/WireGuardConfigGenerator/UserControlTree.Designer.cs
+++ b/WireGuardConfigGenerator/UserControlTree.Designer.cs
@@ -59,7 +59,6 @@
Controls.Add(treeView1);
Name = "UserControlTree";
Size = new Size(138, 130);
- Load += UserControlTree_Load;
ResumeLayout(false);
}
diff --git a/WireGuardConfigGenerator/UserControlTree.cs b/WireGuardConfigGenerator/UserControlTree.cs
index c0f89d7..a874e7d 100644
--- a/WireGuardConfigGenerator/UserControlTree.cs
+++ b/WireGuardConfigGenerator/UserControlTree.cs
@@ -19,12 +19,12 @@ public partial class UserControlTree : UserControl
this.Settings = new();
}
- private void UserControlTree_Load(object sender, EventArgs e)
+ public async Task LoadAsync(string path, string password)
{
if (this.ParentForm is Form1 form)
- form.FormClosing += (s, e) => root.Save();
+ form.FormClosing += async (s, e) => await root.SaveAsync(path, password);
- TreeHelpers.LoadTree(this.treeView1, root);
+ await TreeHelpers.LoadTreeAsync(this.treeView1, root, path, password);
}