diff --git a/.github/workflows/NtgeApp.yaml b/.github/workflows/NtgeApp.yaml new file mode 100644 index 0000000..f395fc0 --- /dev/null +++ b/.github/workflows/NtgeApp.yaml @@ -0,0 +1,79 @@ +name: NtgeApp + +on: + push: + paths: + - "ntge-core/**" + - "NtgeCore.Net/**" + - "NtgeApp/**" + pull_request: + paths: + - "ntge-core/**" + - "NtgeCore.Net/**" + - "NtgeApp/**" + +jobs: + + linux: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.201 + - name: Install dependencies + working-directory: ./NtgeApp + run: dotnet restore + - name: Publish + working-directory: ./NtgeApp + run: dotnet publish -c Release -r linux-x64 --self-contained true /p:PublishSingleFile=true + - name: Archive publish artifacts + uses: actions/upload-artifact@v1 + with: + name: NtgeApp_linux-x64 + path: NtgeApp/bin/Release/netcoreapp3.1/linux-x64/publish + + win: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v1 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.201 + - name: Install dependencies + working-directory: ./NtgeApp + run: dotnet restore + - name: Publish + working-directory: ./NtgeApp + run: dotnet publish -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true + - name: Archive publish artifacts + uses: actions/upload-artifact@v1 + with: + name: NtgeApp_win-x64 + path: NtgeApp/bin/Release/netcoreapp3.1/win-x64/publish + + osx: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v1 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.201 + - name: Install dependencies + working-directory: ./NtgeApp + run: dotnet restore + - name: Publish + working-directory: ./NtgeApp + run: dotnet publish -c Release -r osx-x64 --self-contained true /p:PublishSingleFile=true + - name: Archive publish artifacts + uses: actions/upload-artifact@v1 + with: + name: NtgeApp_osx-x64 + path: NtgeApp/bin/Release/netcoreapp3.1/osx-x64/publish \ No newline at end of file diff --git a/NtgeApp/.gitignore b/NtgeApp/.gitignore new file mode 100644 index 0000000..dbe7c8f --- /dev/null +++ b/NtgeApp/.gitignore @@ -0,0 +1,337 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +## Visual Studio Code specific files and folder +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.jsons + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ diff --git a/NtgeApp/App.xaml b/NtgeApp/App.xaml new file mode 100644 index 0000000..2483457 --- /dev/null +++ b/NtgeApp/App.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + #2B579A + + \ No newline at end of file diff --git a/NtgeApp/App.xaml.cs b/NtgeApp/App.xaml.cs new file mode 100644 index 0000000..96905e7 --- /dev/null +++ b/NtgeApp/App.xaml.cs @@ -0,0 +1,26 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using PropertyChanged; + +namespace NtgeApp +{ + [DoNotNotify] + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } + } +} \ No newline at end of file diff --git a/NtgeApp/Common/Consts.cs b/NtgeApp/Common/Consts.cs new file mode 100644 index 0000000..0999fb9 --- /dev/null +++ b/NtgeApp/Common/Consts.cs @@ -0,0 +1,11 @@ +using System; + +namespace NtgeApp.Common +{ + public class Consts + { + public const string NtgeFolder = ".ntge"; + + public static string HomeDir => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + } +} \ No newline at end of file diff --git a/NtgeApp/DataSource/KeySource.cs b/NtgeApp/DataSource/KeySource.cs new file mode 100644 index 0000000..1f0ef10 --- /dev/null +++ b/NtgeApp/DataSource/KeySource.cs @@ -0,0 +1,68 @@ +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using Avalonia.Threading; +using NtgeApp.Common; +using NtgeApp.Models; + +namespace NtgeApp.DataSource +{ + public class KeySource + { + private readonly FileSystemWatcher _watcher; + + private KeySource() + { + LoadKeys(); + + _watcher = new FileSystemWatcher(); + _watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.CreationTime | NotifyFilters.LastWrite; + _watcher.Path = Path.Combine(Consts.HomeDir, Consts.NtgeFolder); + _watcher.Renamed += WatcherOnRenamed; + _watcher.Created += WatcherOnCreated; + _watcher.Deleted += WatcherOnDeleted; + _watcher.EnableRaisingEvents = true; + } + + public static KeySource Instance { get; } = new KeySource(); + public ObservableCollection Items { get; } = new ObservableCollection(); + + private void LoadKeys() + { + Items.Clear(); + var path = Path.Combine(Consts.HomeDir, Consts.NtgeFolder); + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + var files = Directory.GetFiles(path) + .Select(it => (path: it, name: Path.GetFileNameWithoutExtension(it))) + .Where(it => !string.IsNullOrEmpty(it.name)) + .GroupBy(it => it.name) + .Where(it => it.Count() == 2 && File.Exists(it.FirstOrDefault().path + ".pub")) + .SelectMany(it => it.Where(tuple => !tuple.path.EndsWith(".pub"))) + .Select(it => it.path); + + foreach (var file in files) + { + Items.Add(new KeyModel(file)); + } + } + + private void WatcherOnDeleted(object sender, FileSystemEventArgs e) + { + Dispatcher.UIThread.Post(LoadKeys); + } + + private void WatcherOnCreated(object sender, FileSystemEventArgs e) + { + Dispatcher.UIThread.Post(LoadKeys); + } + + private void WatcherOnRenamed(object sender, RenamedEventArgs e) + { + Dispatcher.UIThread.Post(LoadKeys); + } + } +} \ No newline at end of file diff --git a/NtgeApp/Dialogs/CreateKeyDialog.xaml b/NtgeApp/Dialogs/CreateKeyDialog.xaml new file mode 100644 index 0000000..d5f060e --- /dev/null +++ b/NtgeApp/Dialogs/CreateKeyDialog.xaml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NtgeApp/Dialogs/CreateKeyDialog.xaml.cs b/NtgeApp/Dialogs/CreateKeyDialog.xaml.cs new file mode 100644 index 0000000..0a5dc90 --- /dev/null +++ b/NtgeApp/Dialogs/CreateKeyDialog.xaml.cs @@ -0,0 +1,76 @@ +using System.IO; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; +using NtgeApp.Common; +using NtgeCore.Net.Ed25519; +using PropertyChanged; + +namespace NtgeApp.Dialogs +{ + [DoNotNotify] + public class CreateKeyDialog : Dialog + { + public CreateKeyDialog() + { + InitializeComponent(); + var textBox = this.Find("CreateKeyTextBox"); + textBox.KeyDown += TextBoxOnKeyDown; + Dispatcher.UIThread.Post(() => textBox.Focus()); + } + + private async void TextBoxOnKeyDown(object? sender, KeyEventArgs e) + { + if (!(sender is TextBox textBox)) + { + return; + } + + if (e.Key == Key.Return) + { + e.Handled = true; + if (!string.IsNullOrEmpty(textBox.Text)) + { + textBox.IsEnabled = false; + await CreateKey(); + } + } + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void OnCancelClicked(object? sender, RoutedEventArgs e) + { + Close(); + } + + private async void OnOkClicked(object? sender, RoutedEventArgs e) + { + await CreateKey(); + } + + private async Task CreateKey() + { + var textBlock = this.Find("CreateKeyTextBox"); + var text = textBlock.Text; + if (string.IsNullOrEmpty(text)) + { + return; + } + + using var keypair = Ed25519Keypair.New(); + using var publicKey = keypair.PublicKey; + using var privateKey = keypair.PrivateKey; + await File.WriteAllTextAsync(Path.Combine(Consts.HomeDir, Consts.NtgeFolder, text), privateKey.Serialize()); + await File.WriteAllTextAsync(Path.Combine(Consts.HomeDir, Consts.NtgeFolder, $"{text}.pub"), + publicKey.Serialize()); + Close(); + } + } +} \ No newline at end of file diff --git a/NtgeApp/Dialogs/Dialog.cs b/NtgeApp/Dialogs/Dialog.cs new file mode 100644 index 0000000..b58a1c0 --- /dev/null +++ b/NtgeApp/Dialogs/Dialog.cs @@ -0,0 +1,27 @@ +using Avalonia.Controls; +using Avalonia.Input; +using PropertyChanged; + +namespace NtgeApp.Dialogs +{ + [DoNotNotify] + public class Dialog : Window + { + public Dialog() + { + SystemDecorations = SystemDecorations.BorderOnly; + WindowStartupLocation = WindowStartupLocation.CenterOwner; + ShowInTaskbar = false; + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (e.Key == Key.Escape) + { + e.Handled = true; + Close(); + } + } + } +} \ No newline at end of file diff --git a/NtgeApp/Dialogs/KeyPickerDialog.xaml b/NtgeApp/Dialogs/KeyPickerDialog.xaml new file mode 100644 index 0000000..d040146 --- /dev/null +++ b/NtgeApp/Dialogs/KeyPickerDialog.xaml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NtgeApp/Dialogs/KeyPickerDialog.xaml.cs b/NtgeApp/Dialogs/KeyPickerDialog.xaml.cs new file mode 100644 index 0000000..3e7cdc0 --- /dev/null +++ b/NtgeApp/Dialogs/KeyPickerDialog.xaml.cs @@ -0,0 +1,58 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using NtgeApp.Models; +using NtgeApp.ViewModels; +using PropertyChanged; + +namespace NtgeApp.Dialogs +{ + [DoNotNotify] + public class KeyPickerDialog : Dialog + { + public KeyPickerDialog() + { + InitializeComponent(); + } + + private void OnItemDoubleTapped(object? sender, RoutedEventArgs e) + { + if (sender is Grid view && view.DataContext is KeyModel model) + { + e.Handled = true; + Close(model); + } + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void OnCancelClicked(object? sender, RoutedEventArgs e) + { + Close(); + } + + private void OnOkClicked(object? sender, RoutedEventArgs e) + { + if (!(DataContext is KeyPickerViewModel viewModel)) + { + return; + } + + var tabControl = this.Find("PickerTabControl"); + if (tabControl.SelectedIndex == 0) + { + if (viewModel.SelectedModel != null) + { + Close(viewModel.SelectedModel); + } + } + else + { + Close(viewModel.CustomKeyContent); + } + } + } +} \ No newline at end of file diff --git a/NtgeApp/FluentWindow.cs b/NtgeApp/FluentWindow.cs new file mode 100644 index 0000000..cce34fb --- /dev/null +++ b/NtgeApp/FluentWindow.cs @@ -0,0 +1,49 @@ +using Avalonia.Styling; +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Platform; +using Avalonia.Controls.Primitives; +using PropertyChanged; + +namespace Avalonia.Controls +{ + [DoNotNotify] + public class FluentWindow : Window, IStyleable + { + Type IStyleable.StyleKey => typeof(Window); + + public FluentWindow() + { + ExtendClientAreaToDecorationsHint = true; + ExtendClientAreaTitleBarHeightHint = -1; + + TransparencyLevelHint = WindowTransparencyLevel.AcrylicBlur; + + this.GetObservable(WindowStateProperty) + .Subscribe(x => + { + PseudoClasses.Set(":maximized", x == WindowState.Maximized); + PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); + }); + + this.GetObservable(IsExtendedIntoWindowDecorationsProperty) + .Subscribe(x => + { + if (!x) + { + SystemDecorations = SystemDecorations.Full; + TransparencyLevelHint = WindowTransparencyLevel.Blur; + } + }); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + ExtendClientAreaChromeHints = + ExtendClientAreaChromeHints.PreferSystemChrome | + ExtendClientAreaChromeHints.OSXThickTitleBar; + } + } +} diff --git a/NtgeApp/FodyWeavers.xml b/NtgeApp/FodyWeavers.xml new file mode 100644 index 0000000..4e68ed1 --- /dev/null +++ b/NtgeApp/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/NtgeApp/FodyWeavers.xsd b/NtgeApp/FodyWeavers.xsd new file mode 100644 index 0000000..221aeb8 --- /dev/null +++ b/NtgeApp/FodyWeavers.xsd @@ -0,0 +1,64 @@ + + + + + + + + + + + Used to control if the On_PropertyName_Changed feature is enabled. + + + + + Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form. + + + + + Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project. + + + + + Used to control if equality checks should use the Equals method resolved from the base class. + + + + + Used to control if equality checks should use the static Equals method resolved from the base class. + + + + + Used to turn off build warnings from this weaver. + + + + + Used to turn off build warnings about mismatched On_PropertyName_Changed methods. + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/NtgeApp/MainWindow.xaml b/NtgeApp/MainWindow.xaml new file mode 100644 index 0000000..70b642a --- /dev/null +++ b/NtgeApp/MainWindow.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NtgeApp/MainWindow.xaml.cs b/NtgeApp/MainWindow.xaml.cs new file mode 100644 index 0000000..7e1fe16 --- /dev/null +++ b/NtgeApp/MainWindow.xaml.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using PropertyChanged; + +namespace NtgeApp +{ + [DoNotNotify] + public class MainWindow : FluentWindow + { + public MainWindow() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} \ No newline at end of file diff --git a/NtgeApp/Models/DecryptKeyModel.cs b/NtgeApp/Models/DecryptKeyModel.cs new file mode 100644 index 0000000..10e9d95 --- /dev/null +++ b/NtgeApp/Models/DecryptKeyModel.cs @@ -0,0 +1,22 @@ +using System; +using NtgeCore.Net.X25519; + +namespace NtgeApp.Models +{ + public class DecryptKeyModel : IDisposable + { + public DecryptKeyModel(string content, X25519PrivateKey x25519PrivateKey) + { + Content = content; + X25519PrivateKey = x25519PrivateKey; + } + + public string Content { get; } + public X25519PrivateKey X25519PrivateKey { get; } + + public void Dispose() + { + X25519PrivateKey.Dispose(); + } + } +} \ No newline at end of file diff --git a/NtgeApp/Models/EncryptKeyModel.cs b/NtgeApp/Models/EncryptKeyModel.cs new file mode 100644 index 0000000..00fe9bf --- /dev/null +++ b/NtgeApp/Models/EncryptKeyModel.cs @@ -0,0 +1,22 @@ +using System; +using NtgeCore.Net.X25519; + +namespace NtgeApp.Models +{ + public class EncryptKeyModel : IDisposable + { + public EncryptKeyModel(string content, X25519PublicKey x25519PublicKey) + { + Content = content; + X25519PublicKey = x25519PublicKey; + } + + public string Content { get; } + public X25519PublicKey X25519PublicKey { get; } + + public void Dispose() + { + X25519PublicKey.Dispose(); + } + } +} \ No newline at end of file diff --git a/NtgeApp/Models/KeyModel.cs b/NtgeApp/Models/KeyModel.cs new file mode 100644 index 0000000..12e46e8 --- /dev/null +++ b/NtgeApp/Models/KeyModel.cs @@ -0,0 +1,55 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using PropertyChanged; + +namespace NtgeApp.Models +{ + public class KeyModel : INotifyPropertyChanged + { + public KeyModel(string path) + { + Path = path; + } + + public string Path { get; set; } + + [DependsOn(nameof(Path))] + public string Name => System.IO.Path.GetFileName(Path); + + [DependsOn(nameof(Path))] + public FileInfo FileInfo => new FileInfo(Path); + + // public string Content { get; set; } + + public string? PublicKeyContent { get; set; } + public string? PrivateKeyContent { get; set; } + + public event PropertyChangedEventHandler? PropertyChanged; + + public async Task EnsurePublicKeyContent() + { + if (string.IsNullOrEmpty(PublicKeyContent)) + { + var result = await File.ReadAllTextAsync(Path + ".pub"); + PublicKeyContent = result.Trim(); + } + } + + public async Task EnsurePrivateKeyContent() + { + if (string.IsNullOrEmpty(PrivateKeyContent)) + { + var result = await File.ReadAllTextAsync(Path); + PrivateKeyContent = result.Trim(); + } + } + + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} \ No newline at end of file diff --git a/NtgeApp/Models/TabItemModel.cs b/NtgeApp/Models/TabItemModel.cs new file mode 100644 index 0000000..9451c9d --- /dev/null +++ b/NtgeApp/Models/TabItemModel.cs @@ -0,0 +1,18 @@ +using Avalonia.Media; + +namespace NtgeApp.Models +{ + internal class TabItemModel + { + public TabItemModel(string title, GeometryDrawing? icon, object content) + { + Title = title; + Icon = icon; + Content = content; + } + + public string Title { get; } + public GeometryDrawing? Icon { get; } + public object Content { get; } + } +} \ No newline at end of file diff --git a/NtgeApp/NtgeApp.csproj b/NtgeApp/NtgeApp.csproj new file mode 100644 index 0000000..2919db5 --- /dev/null +++ b/NtgeApp/NtgeApp.csproj @@ -0,0 +1,36 @@ + + + WinExe + netcoreapp3.1 + preview + enable + True + + + + + + + %(Filename) + + + Designer + + + Designer + + + + + + + + + + + + + + + + diff --git a/NtgeApp/Program.cs b/NtgeApp/Program.cs new file mode 100644 index 0000000..d22c091 --- /dev/null +++ b/NtgeApp/Program.cs @@ -0,0 +1,24 @@ +using Avalonia; + +namespace NtgeApp +{ + internal class Program + { + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + public static void Main(string[] args) + { + BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + } + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + { + return AppBuilder.Configure() + .UsePlatformDetect() + .LogToDebug(); + } + } +} \ No newline at end of file diff --git a/NtgeApp/Resources/Icons.xaml b/NtgeApp/Resources/Icons.xaml new file mode 100644 index 0000000..e264f23 --- /dev/null +++ b/NtgeApp/Resources/Icons.xaml @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/NtgeApp/Resources/SideBar.xaml b/NtgeApp/Resources/SideBar.xaml new file mode 100644 index 0000000..4155b7b --- /dev/null +++ b/NtgeApp/Resources/SideBar.xaml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + diff --git a/NtgeApp/ViewModels/DecryptViewModel.cs b/NtgeApp/ViewModels/DecryptViewModel.cs new file mode 100644 index 0000000..3597b5b --- /dev/null +++ b/NtgeApp/ViewModels/DecryptViewModel.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using NtgeApp.Models; +using NtgeCore.Net; +using NtgeCore.Net.Ed25519; +using NtgeCore.Net.Message; +using PropertyChanged; + +namespace NtgeApp.ViewModels +{ + public class DecryptViewModel : ViewModelBase + { + public DecryptKeyModel? DecryptKeyModel { get; private set; } + + public string? Input { get; set; } + + public string? Output { get; set; } + + public void OnInputChanged() + { + Decrypt(); + } + + public void OnDecryptKeyModelChanged() + { + Decrypt(); + } + + void Decrypt() + { + if (string.IsNullOrEmpty(Input) || DecryptKeyModel == null) + { + Output = string.Empty; + return; + } + try + { + using var message = NtgeMessage.Deserialize(Input); + using var decryptor = Decryptor.New(message); + using var fileKey = decryptor.GetFileKey(DecryptKeyModel.X25519PrivateKey); + Output = decryptor.DecryptPayload(fileKey); + } + catch (NtgeException e) + { + Output = e.Message; + } + } + + public void SetKey(string privateKeyContent) + { + DecryptKeyModel?.Dispose(); + try + { + using var privateKey = Ed25519PrivateKey.Deserialize(privateKeyContent); + DecryptKeyModel = new DecryptKeyModel(privateKeyContent, privateKey.ToX25519()); + } + catch (NtgeException e) + { + DecryptKeyModel = null; + } + + } + } +} \ No newline at end of file diff --git a/NtgeApp/ViewModels/EncryptViewModel.cs b/NtgeApp/ViewModels/EncryptViewModel.cs new file mode 100644 index 0000000..6bd479d --- /dev/null +++ b/NtgeApp/ViewModels/EncryptViewModel.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using NtgeApp.Models; +using NtgeCore.Net; +using NtgeCore.Net.Ed25519; +using NtgeCore.Net.Message; +using NtgeCore.Net.X25519; +using PropertyChanged; + +namespace NtgeApp.ViewModels +{ + public class EncryptViewModel : ViewModelBase + { + public EncryptViewModel() + { + Keys.CollectionChanged += KeysOnCollectionChanged; + } + + public ObservableCollection Keys { get; } = new ObservableCollection(); + + public string? Input { get; set; } + + [DependsOn(nameof(Input))] + public string Output + { + get + { + if (string.IsNullOrEmpty(Input)) + { + return string.Empty; + } + + using var encryptor = Encryptor.New(Keys.Select(it => it.X25519PublicKey).ToArray()); + using var message = encryptor.EncryptPlaintext(Input); + return message.Serialize(); + } + } + + private void KeysOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(Output); + } + + public void AddKey(string publicKey) + { + if (Keys.Any(it => it.Content == publicKey)) + { + return; + } + + try + { + using var ed25519PublicKey = Ed25519PublicKey.Deserialize(publicKey); + Keys.Add(new EncryptKeyModel(publicKey, ed25519PublicKey.ToX25519())); + } + catch (NtgeException e) + { + } + } + + public void RemoveKey(EncryptKeyModel publicKey) + { + Keys.Remove(publicKey); + publicKey?.Dispose(); + } + } +} \ No newline at end of file diff --git a/NtgeApp/ViewModels/KeyPickerViewModel.cs b/NtgeApp/ViewModels/KeyPickerViewModel.cs new file mode 100644 index 0000000..f120433 --- /dev/null +++ b/NtgeApp/ViewModels/KeyPickerViewModel.cs @@ -0,0 +1,10 @@ +using NtgeApp.Models; + +namespace NtgeApp.ViewModels +{ + public class KeyPickerViewModel : KeysViewModel + { + public KeyModel? SelectedModel { get; set; } + public string? CustomKeyContent { get; set; } + } +} \ No newline at end of file diff --git a/NtgeApp/ViewModels/KeysViewModel.cs b/NtgeApp/ViewModels/KeysViewModel.cs new file mode 100644 index 0000000..ec7ecc9 --- /dev/null +++ b/NtgeApp/ViewModels/KeysViewModel.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using NtgeApp.Common; +using NtgeApp.DataSource; +using NtgeApp.Models; + +namespace NtgeApp.ViewModels +{ + public class KeysViewModel : ViewModelBase + { + public ObservableCollection Items => KeySource.Instance.Items; + } +} \ No newline at end of file diff --git a/NtgeApp/ViewModels/MainViewModel.cs b/NtgeApp/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..b0ed807 --- /dev/null +++ b/NtgeApp/ViewModels/MainViewModel.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using NtgeApp.Models; +using NtgeApp.Views; + +namespace NtgeApp.ViewModels +{ + internal class MainViewModel : ViewModelBase + { + public TabItemModel[] Tabs { get; } = + { + new TabItemModel("Keys", Application.Current.FindResource("BoxIcons.RegularKey") as GeometryDrawing, + new KeysView()), + new TabItemModel("Encrypt", + Application.Current.FindResource("MaterialDesign.EnhancedEncryption") as GeometryDrawing, + new EncryptView()), + new TabItemModel("Decrypt", + Application.Current.FindResource("Ionicons.UnlockiOS") as GeometryDrawing, + new DecryptView()) + }; + } +} \ No newline at end of file diff --git a/NtgeApp/ViewModels/ViewModelBase.cs b/NtgeApp/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..ab3d85c --- /dev/null +++ b/NtgeApp/ViewModels/ViewModelBase.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace NtgeApp.ViewModels +{ + public abstract class ViewModelBase : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} \ No newline at end of file diff --git a/NtgeApp/Views/DecryptView.xaml b/NtgeApp/Views/DecryptView.xaml new file mode 100644 index 0000000..b06e225 --- /dev/null +++ b/NtgeApp/Views/DecryptView.xaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NtgeApp/Views/DecryptView.xaml.cs b/NtgeApp/Views/DecryptView.xaml.cs new file mode 100644 index 0000000..6fe43f8 --- /dev/null +++ b/NtgeApp/Views/DecryptView.xaml.cs @@ -0,0 +1,56 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using NtgeApp.Dialogs; +using NtgeApp.Models; +using NtgeApp.ViewModels; +using PropertyChanged; + +namespace NtgeApp.Views +{ + [DoNotNotify] + public class DecryptView : UserControl + { + public DecryptView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + + + private async void OnSelectKeyClick(object? sender, RoutedEventArgs e) + { + if (!(DataContext is DecryptViewModel viewModel)) + { + return; + } + + var dialog = new KeyPickerDialog(); + if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + var result = await dialog.ShowDialog(desktop.MainWindow); + switch (result) + { + case KeyModel keyModel: + await keyModel.EnsurePrivateKeyContent(); + if (!string.IsNullOrEmpty(keyModel.PrivateKeyContent)) + { + viewModel.SetKey(keyModel.PrivateKeyContent); + } + break; + case string key: + viewModel.SetKey(key); + break; + } + } + } + + } +} \ No newline at end of file diff --git a/NtgeApp/Views/EncryptView.xaml b/NtgeApp/Views/EncryptView.xaml new file mode 100644 index 0000000..12842ad --- /dev/null +++ b/NtgeApp/Views/EncryptView.xaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NtgeApp/Views/EncryptView.xaml.cs b/NtgeApp/Views/EncryptView.xaml.cs new file mode 100644 index 0000000..2c22cef --- /dev/null +++ b/NtgeApp/Views/EncryptView.xaml.cs @@ -0,0 +1,67 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using NtgeApp.Dialogs; +using NtgeApp.Models; +using NtgeApp.ViewModels; +using PropertyChanged; + +namespace NtgeApp.Views +{ + [DoNotNotify] + public class EncryptView : UserControl + { + public EncryptView() + { + InitializeComponent(); + } + + private void OnRemoveClick(object? sender, RoutedEventArgs e) + { + if (!(DataContext is EncryptViewModel viewModel)) + { + return; + } + + if (sender is Button button && button.DataContext is EncryptKeyModel value) + { + viewModel.RemoveKey(value); + } + } + + private async void OnAddClick(object? sender, RoutedEventArgs e) + { + if (!(DataContext is EncryptViewModel viewModel)) + { + return; + } + + var dialog = new KeyPickerDialog(); + if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + var result = await dialog.ShowDialog(desktop.MainWindow); + switch (result) + { + case KeyModel keyModel: + await keyModel.EnsurePublicKeyContent(); + if (!string.IsNullOrEmpty(keyModel.PublicKeyContent)) + { + viewModel.AddKey(keyModel.PublicKeyContent); + } + break; + case string key: + viewModel.AddKey(key); + break; + } + } + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} \ No newline at end of file diff --git a/NtgeApp/Views/KeysView.xaml b/NtgeApp/Views/KeysView.xaml new file mode 100644 index 0000000..53b6b3f --- /dev/null +++ b/NtgeApp/Views/KeysView.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NtgeApp/Views/KeysView.xaml.cs b/NtgeApp/Views/KeysView.xaml.cs new file mode 100644 index 0000000..e90f173 --- /dev/null +++ b/NtgeApp/Views/KeysView.xaml.cs @@ -0,0 +1,33 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using NtgeApp.Dialogs; +using PropertyChanged; + +namespace NtgeApp.Views +{ + [DoNotNotify] + public class KeysView : UserControl + { + public KeysView() + { + InitializeComponent(); + } + + private async void OnCreateClicked(object? sender, RoutedEventArgs e) + { + if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + var dialog = new CreateKeyDialog(); + await dialog.ShowDialog(desktop.MainWindow); + } + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} \ No newline at end of file diff --git a/NtgeApp/nuget.config b/NtgeApp/nuget.config new file mode 100644 index 0000000..7c07e22 --- /dev/null +++ b/NtgeApp/nuget.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/NtgeCore.Net/src/Message/Decryptor.cs b/NtgeCore.Net/src/Message/Decryptor.cs index 19219bf..0732ae0 100644 --- a/NtgeCore.Net/src/Message/Decryptor.cs +++ b/NtgeCore.Net/src/Message/Decryptor.cs @@ -21,13 +21,23 @@ public bool VerifyMessageMac(X25519FileKey fileKey) public X25519FileKey GetFileKey(X25519PrivateKey privateKey) { - return new X25519FileKey(Native.messageDecryptorDecryptFileKey(Ptr, privateKey.Ptr)); + var ptr = Native.messageDecryptorDecryptFileKey(Ptr, privateKey.Ptr); + if (ptr == IntPtr.Zero) + { + throw new NtgeException("Can not get file key"); + } + return new X25519FileKey(ptr); } public string DecryptPayload(X25519FileKey fileKey) { - using var result = Native.messageDecryptorDecryptPayload(Ptr, fileKey.Ptr); - return result.AsString(); + var ptr = Native.messageDecryptorDecryptPayload(Ptr, fileKey.Ptr); + if (ptr == IntPtr.Zero) + { + throw new NtgeException("Can not decrypt payload"); + } + using var stringHandle = new StringHandle(ptr); + return stringHandle.AsString(); } public string DecryptPayloadExtra(X25519FileKey fileKey) diff --git a/NtgeCore.Net/src/Native.cs b/NtgeCore.Net/src/Native.cs index 0b16fae..9ff8754 100644 --- a/NtgeCore.Net/src/Native.cs +++ b/NtgeCore.Net/src/Native.cs @@ -81,7 +81,7 @@ internal class Native public static extern IntPtr messageDecryptorDecryptFileKey(IntPtr decryptor_ptr, IntPtr private_key_ptr); [DllImport(LIB_NAME)] - public static extern StringHandle messageDecryptorDecryptPayload(IntPtr decryptor_ptr, IntPtr file_key_ptr); + public static extern IntPtr messageDecryptorDecryptPayload(IntPtr decryptor_ptr, IntPtr file_key_ptr); [DllImport(LIB_NAME)] [return: MarshalAs(UnmanagedType.I1)] diff --git a/NtgeCore.Net/src/StringHandle.cs b/NtgeCore.Net/src/StringHandle.cs index 64b0751..688e8ea 100644 --- a/NtgeCore.Net/src/StringHandle.cs +++ b/NtgeCore.Net/src/StringHandle.cs @@ -6,6 +6,11 @@ namespace NtgeCore.Net { internal class StringHandle : SafeHandle { + public StringHandle(IntPtr ptr) : this() + { + SetHandle(ptr); + } + public StringHandle() : base(IntPtr.Zero, true) {