Compare commits

...

10 Commits

Author SHA1 Message Date
4f9e8e084a Update launchSettings.json 2021-06-10 19:43:15 +02:00
01a54f32af Hide token in client app 2021-06-08 22:54:40 +02:00
8b8447649a Implement Client 2021-06-08 22:31:13 +02:00
c11911c4a7 Fix ApplicationsManager
Missing return
2021-06-08 22:30:23 +02:00
eb5849c16d Add log to gRPC api 2021-06-07 20:16:27 +02:00
08a954f836 Move Protos to Shared library 2021-06-07 20:11:44 +02:00
c37e8d3fb4 Initial client version 2021-06-07 19:49:49 +02:00
1d6c8de71b Add AkariApi gRPC service 2021-06-07 19:49:35 +02:00
adbee34498 Change stackalloc size in Security 2021-06-07 19:49:06 +02:00
0f88e82e28 Make VerifyToken public in IApplicationsManager 2021-06-07 19:48:43 +02:00
20 changed files with 512 additions and 63 deletions

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.6" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.6" />
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.6" />
<PackageReference Include="Grpc.Net.Client" Version="2.37.0" />
<PackageReference Include="MessageBox.Avalonia" Version="1.3.1" />
<PackageReference Include="XamlNameReferenceGenerator" Version="1.2.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Akari.Prototype.Shared\Akari.Prototype.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,7 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Akari.Prototype.Client.App">
<Application.Styles>
<FluentTheme Mode="Light"/>
</Application.Styles>
</Application>

View File

@@ -0,0 +1,24 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace Akari.Prototype.Client
{
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();
}
}
}

View File

@@ -0,0 +1,80 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Akari.Prototype.Client.MainWindow"
Title="Akari.Prototype.Client"
Width="600"
Height="350"
ExtendClientAreaToDecorationsHint="True"
TransparencyLevelHint="AcrylicBlur"
ExtendClientAreaTitleBarHeightHint="-1"
Background="{x:Null}">
<Panel>
<ExperimentalAcrylicBorder IsHitTestVisible="False">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial BackgroundSource="Digger" TintColor="#F8F8F0" TintOpacity="1" MaterialOpacity="0.6" />
</ExperimentalAcrylicBorder.Material>
</ExperimentalAcrylicBorder>
<RelativePanel VerticalAlignment="Center" Margin="20">
<TextBlock x:Name="TxtName"
Margin="5, 15"
Text="Application Name:" />
<TextBox x:Name="BoxName"
RelativePanel.RightOf="TxtName"
RelativePanel.AlignRightWithPanel="True"
RelativePanel.AlignVerticalCenterWith="TxtName" />
<TextBlock x:Name="TxtToken"
RelativePanel.Below="TxtName"
RelativePanel.AlignRightWith="TxtName"
Margin="5, 15"
Text="Token:" />
<TextBox x:Name="BoxToken"
RelativePanel.RightOf="TxtToken"
RelativePanel.AlignRightWithPanel="True"
RelativePanel.AlignLeftWith="BoxName"
RelativePanel.AlignVerticalCenterWith="TxtToken"
PasswordChar="*" />
<TextBlock x:Name="TxtMode"
RelativePanel.Below="TxtToken"
RelativePanel.AlignRightWith="TxtToken"
Margin="5, 15"
Text="Mode:" />
<ComboBox x:Name="CbxMode"
RelativePanel.RightOf="TxtMode"
RelativePanel.AlignVerticalCenterWith="TxtMode"
MinWidth="100"
SelectedIndex="0">
<ComboBoxItem>Encrypt</ComboBoxItem>
<ComboBoxItem>Decrypt</ComboBoxItem>
</ComboBox>
<Button x:Name="BtnPickFile"
RelativePanel.RightOf="CbxMode"
RelativePanel.AlignVerticalCenterWith="TxtMode"
Margin="15, 0, 5, 0"
Content="Pick File"
Click="OnPickFile" />
<TextBlock x:Name="TxtFile"
RelativePanel.RightOf="BtnPickFile"
RelativePanel.AlignRightWithPanel="True"
RelativePanel.AlignVerticalCenterWith="BtnPickFile"
MaxWidth="250"
TextTrimming="CharacterEllipsis"
Text="N/A" />
<Button x:Name="BtnSubmit"
RelativePanel.Below="TxtMode"
RelativePanel.AlignLeftWith="BoxToken"
RelativePanel.AlignRightWith="BoxToken"
HorizontalContentAlignment="Center"
HorizontalAlignment="Stretch"
Margin="0, 5"
Content="Submit"
Click="OnSubmit" />
</RelativePanel>
</Panel>
</Window>

View File

@@ -0,0 +1,134 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Akari.Prototype.Shared.Protos;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Google.Protobuf;
using Grpc.Net.Client;
using MessageBox.Avalonia;
namespace Akari.Prototype.Client
{
public partial class MainWindow : Window, IDisposable
{
public const string Hostname = "https://localhost:5001";
private readonly GrpcChannel _channel;
private string? _file = null;
public MainWindow()
{
InitializeComponent();
_channel = GrpcChannel.ForAddress(Hostname);
}
private async void OnPickFile(object sender, RoutedEventArgs e)
{
var mode = GetMode();
var dialog = new OpenFileDialog()
{
AllowMultiple = false,
Title = $"Pick a file to {mode}"
};
if (await dialog.ShowAsync(this) is string[] { Length: 1 } res)
{
_file = res[0];
TxtFile.Text = Path.GetFileName(_file);
}
}
private async void OnSubmit(object sender, RoutedEventArgs e)
{
if (_file is null)
{
return;
}
if (string.IsNullOrWhiteSpace(BoxName.Text))
{
await MessageBoxManager.GetMessageBoxStandardWindow("Error", $"Invalid application name", icon: MessageBox.Avalonia.Enums.Icon.Error).Show();
return;
}
if (string.IsNullOrWhiteSpace(BoxToken.Text))
{
await MessageBoxManager.GetMessageBoxStandardWindow("Error", $"Invalid token", icon: MessageBox.Avalonia.Enums.Icon.Error).Show();
return;
}
var client = new AkariApi.AkariApiClient(_channel);
// Decrypt
if (GetMode() == Mode.Decrypt)
{
var response = await client.DecryptAsync(new DecryptRequest()
{
Application = BoxName.Text,
Token = BoxToken.Text,
Encrypted = ByteString.CopyFrom(File.ReadAllBytes(_file))
});
if (response.ResponseCase == DecryptResponse.ResponseOneofCase.ErrorMessage)
{
await MessageBoxManager.GetMessageBoxStandardWindow("Error", $"Error during decryption, server response: {response.ErrorMessage}", icon: MessageBox.Avalonia.Enums.Icon.Error).Show();
}
else
{
string outPath = Path.ChangeExtension(_file, null);
File.WriteAllBytes(outPath, response.Plain.ToByteArray());
await MessageBoxManager.GetMessageBoxStandardWindow("Success", $"Saved decrypted file to {outPath}\n").Show();
}
}
// Encrypt
else
{
var response = await client.EncryptAsync(new EncryptRequest()
{
Application = BoxName.Text,
Token = BoxToken.Text,
Plain = ByteString.CopyFrom(File.ReadAllBytes(_file))
});
if (response.ResponseCase == EncryptResponse.ResponseOneofCase.ErrorMessage)
{
await MessageBoxManager.GetMessageBoxStandardWindow("Error", $"Error during encryption, server response: {response.ErrorMessage}", icon: MessageBox.Avalonia.Enums.Icon.Error).Show();
}
else
{
string outPath = $"{_file}.enc";
File.WriteAllBytes(outPath, response.Encrypted.ToByteArray());
await MessageBoxManager.GetMessageBoxStandardWindow("Success", $"Saved encrypted file to {outPath}").Show();
}
}
}
private Mode GetMode()
{
if (CbxMode.SelectedItem is ComboBoxItem { Content: string mode })
{
return Enum.Parse<Mode>(mode);
}
throw new Exception("Invalid mode in CbxMode");
}
public void Dispose()
{
_channel.Dispose();
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Akari.Prototype.Client
{
enum Mode
{
Encrypt,
Decrypt
}
}

View File

@@ -0,0 +1,22 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
namespace Akari.Prototype.Client
{
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()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
To use the Avalonia CI feed to get unstable packages, move this file to the root of your solution.
-->
<configuration>
<packageSources>
<add key="AvaloniaCI" value="https://www.myget.org/F/avalonia-ci/api/v2" />
</packageSources>
</configuration>

View File

@@ -1,13 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CliFx" Version="2.0.4" />
<PackageReference Include="Grpc.AspNetCore" Version="2.34.0" />
@@ -15,4 +11,8 @@
<PackageReference Include="Spectre.Console" Version="0.39.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Akari.Prototype.Shared\Akari.Prototype.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,13 +1,13 @@
{
{
"profiles": {
"Akari.Prototype.Server": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": false,
"applicationUrl": "http://localhost:5000;https://localhost:5001",
"workingDirectory": ".\\bin\\Debug\\net5.0",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"dotnetRunMessages": "true",
"applicationUrl": "http://localhost:5000;https://localhost:5001"
}
}
}
}

View File

@@ -1,21 +0,0 @@
syntax = "proto3";
option csharp_namespace = "Akari.Prototype.Server";
package greet;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply);
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings.
message HelloReply {
string message = 1;
}

View File

@@ -0,0 +1,109 @@
using Akari.Prototype.Server.Services;
using Akari.Prototype.Server.Utils;
using Akari.Prototype.Shared.Protos;
using Google.Protobuf;
using Grpc.Core;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Akari.Prototype.Server
{
public class AkariService : AkariApi.AkariApiBase
{
private readonly ILogger<AkariService> _logger;
private readonly IApplicationsManager _applications;
public AkariService(ILogger<AkariService> logger, IApplicationsManager applications)
{
_logger = logger;
_applications = applications;
}
public override Task<DecryptResponse> Decrypt(DecryptRequest request, ServerCallContext context)
{
_logger.LogDebug($"Received decrypt request: {request}");
if (!_applications.Contains(request.Application))
{
_logger.LogDebug($"Application not found: {request.Application}");
return Task.FromResult(new DecryptResponse()
{
ErrorMessage = "Application not found"
});
}
if (!_applications.VerifyToken(request.Application, request.Token))
{
_logger.LogDebug($"Invalid token: {request.Token}");
return Task.FromResult(new DecryptResponse()
{
ErrorMessage = "Invalid token"
});
}
if (_applications.TryRetrieveKey(request.Application, request.Token, out var key))
{
_logger.LogDebug($"Key retrieved, sending decrypted data...");
return Task.FromResult(new DecryptResponse()
{
Plain = ByteString.CopyFrom(Security.AesGcmDecrypt(key, request.Encrypted.ToByteArray()))
});
}
_logger.LogDebug($"No fingerprint auth found for {request.Application}");
return Task.FromResult(new DecryptResponse()
{
ErrorMessage = "No fingerprint auth found for this application"
});
}
public override Task<EncryptResponse> Encrypt(EncryptRequest request, ServerCallContext context)
{
_logger.LogDebug($"Received encrypt request: {request}");
if (!_applications.Contains(request.Application))
{
_logger.LogDebug($"Application not found: {request.Application}");
return Task.FromResult(new EncryptResponse()
{
ErrorMessage = "Application not found"
});
}
if (!_applications.VerifyToken(request.Application, request.Token))
{
_logger.LogDebug($"Invalid token: {request.Token}");
return Task.FromResult(new EncryptResponse()
{
ErrorMessage = "Wrong token"
});
}
if (_applications.TryRetrieveKey(request.Application, request.Token, out var key))
{
_logger.LogDebug($"Key retrieved, sending decrypted data...");
return Task.FromResult(new EncryptResponse()
{
Encrypted = ByteString.CopyFrom(Security.AesGcmEncrypt(key, request.Plain.ToByteArray()))
});
}
_logger.LogDebug($"No fingerprint auth found for {request.Application}");
return Task.FromResult(new EncryptResponse()
{
ErrorMessage = "No fingerprint auth found for this application"
});
}
}
}

View File

@@ -163,7 +163,7 @@ namespace Akari.Prototype.Server.Services
return true;
}
private bool VerifyToken(string applicationName, string applicationToken)
public bool VerifyToken(string applicationName, string applicationToken)
{
if (!_applications.TryGetValue(applicationName, out var application))
{
@@ -194,6 +194,8 @@ namespace Akari.Prototype.Server.Services
if (!_authManager.TryGetKey(application.Fingerprints, out var fingerprintName, out var fingerprintKey))
{
_logger.LogDebug($"Can't retrieve '{applicationName}' key, no fingerprint auth found");
return false;
}
applicationKey = _keyManager.RetrieveKey(applicationName, fingerprintName, fingerprintKey);

View File

@@ -1,26 +0,0 @@
using Grpc.Core;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Akari.Prototype.Server
{
public class GreeterService : Greeter.GreeterBase
{
private readonly ILogger<GreeterService> _logger;
public GreeterService(ILogger<GreeterService> logger)
{
_logger = logger;
}
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = "Hello " + request.Name
});
}
}
}

View File

@@ -19,6 +19,8 @@ namespace Akari.Prototype.Server.Services
bool AddFingerprint(string applicationName, string fingerprintName);
bool VerifyToken(string applicationName, string applicationToken);
bool TryRetrieveKey(string applicationName, string applicationToken, out AesGcm key);
}
}

View File

@@ -64,7 +64,7 @@ namespace Akari.Prototype.Server
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<GreeterService>();
endpoints.MapGrpcService<AkariService>();
endpoints.MapGet("/", async context =>
{

View File

@@ -87,7 +87,7 @@ namespace Akari.Prototype.Server.Utils
// We write everything into one big array for easier encoding
int encryptedDataLength = 4 + nonceSize + 4 + tagSize + cipherSize;
Span<byte> encryptedData = encryptedDataLength < 1024
Span<byte> encryptedData = encryptedDataLength <= 1024
? stackalloc byte[encryptedDataLength]
: new byte[encryptedDataLength];
@@ -122,7 +122,7 @@ namespace Akari.Prototype.Server.Utils
var cipherBytes = encryptedData.Slice(4 + nonceSize + 4 + tagSize, cipherSize);
// Decrypt
Span<byte> plainBytes = cipherSize < 1024
Span<byte> plainBytes = cipherSize <= 1024
? stackalloc byte[cipherSize]
: new byte[cipherSize];

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.17.2" />
<PackageReference Include="Grpc.Core.Api" Version="2.38.0" />
<PackageReference Include="Grpc.Tools" Version="2.38.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Protos\" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="**/*.proto" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,37 @@
syntax = "proto3";
option csharp_namespace = "Akari.Prototype.Shared.Protos";
package akari;
service AkariApi {
rpc Encrypt (EncryptRequest) returns (EncryptResponse);
rpc Decrypt (DecryptRequest) returns (DecryptResponse);
}
message EncryptRequest {
string application = 1;
string token = 2;
bytes plain = 3;
}
message EncryptResponse {
oneof response {
string error_message = 1;
bytes encrypted = 2;
}
}
message DecryptRequest {
string application = 1;
string token = 2;
bytes encrypted = 3;
}
message DecryptResponse {
oneof response {
string error_message = 1;
bytes plain = 2;
}
}

View File

@@ -3,7 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31321.278
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akari.Prototype.Server", "Akari.Prototype.Server\Akari.Prototype.Server.csproj", "{AFF5FC9F-41B5-4B05-953E-02DBC28833CD}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akari.Prototype.Server", "Akari.Prototype.Server\Akari.Prototype.Server.csproj", "{AFF5FC9F-41B5-4B05-953E-02DBC28833CD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akari.Prototype.Client", "Akari.Prototype.Client\Akari.Prototype.Client.csproj", "{3CEA1E09-B799-42B5-A258-A5E549FEC0ED}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akari.Prototype.Shared", "Akari.Prototype.Shared\Akari.Prototype.Shared.csproj", "{F0555B06-E172-490C-BF5D-AFA8D837358B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -15,6 +19,14 @@ Global
{AFF5FC9F-41B5-4B05-953E-02DBC28833CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AFF5FC9F-41B5-4B05-953E-02DBC28833CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AFF5FC9F-41B5-4B05-953E-02DBC28833CD}.Release|Any CPU.Build.0 = Release|Any CPU
{3CEA1E09-B799-42B5-A258-A5E549FEC0ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3CEA1E09-B799-42B5-A258-A5E549FEC0ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3CEA1E09-B799-42B5-A258-A5E549FEC0ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3CEA1E09-B799-42B5-A258-A5E549FEC0ED}.Release|Any CPU.Build.0 = Release|Any CPU
{F0555B06-E172-490C-BF5D-AFA8D837358B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F0555B06-E172-490C-BF5D-AFA8D837358B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F0555B06-E172-490C-BF5D-AFA8D837358B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F0555B06-E172-490C-BF5D-AFA8D837358B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE