From f233cd0b64f2072694943b844f0be39ff5f072b4 Mon Sep 17 00:00:00 2001 From: gosha20777 Date: Sat, 26 Oct 2019 16:27:25 +0300 Subject: [PATCH] add auth func --- RescuerLaApp/Models/AppStatusInfo.cs | 3 +- RescuerLaApp/Models/Enums.cs | 1 + RescuerLaApp/Models/PasswordHasher.cs | 83 ++++++++++ RescuerLaApp/Program.cs | 2 +- RescuerLaApp/RescuerLaApp.csproj | 1 + .../ViewModels/MainWindowViewModel.cs | 30 +++- RescuerLaApp/Views/MainWindow.xaml | 4 + RescuerLaApp/Views/SignInWindow.xaml | 20 +++ RescuerLaApp/Views/SignInWindow.xaml.cs | 113 +++++++++++++ RescuerLaApp/Views/SignUpWindow.Xaml | 0 RescuerLaApp/Views/SignUpWindow.xaml | 26 +++ RescuerLaApp/Views/SignUpWindow.xaml.cs | 149 ++++++++++++++++++ 12 files changed, 426 insertions(+), 6 deletions(-) create mode 100644 RescuerLaApp/Models/PasswordHasher.cs create mode 100644 RescuerLaApp/Views/SignInWindow.xaml create mode 100644 RescuerLaApp/Views/SignInWindow.xaml.cs create mode 100644 RescuerLaApp/Views/SignUpWindow.Xaml create mode 100644 RescuerLaApp/Views/SignUpWindow.xaml create mode 100644 RescuerLaApp/Views/SignUpWindow.xaml.cs diff --git a/RescuerLaApp/Models/AppStatusInfo.cs b/RescuerLaApp/Models/AppStatusInfo.cs index ff56d68..36f29ac 100755 --- a/RescuerLaApp/Models/AppStatusInfo.cs +++ b/RescuerLaApp/Models/AppStatusInfo.cs @@ -31,7 +31,8 @@ private ISolidColorBrush GetColor() case Status.Ready: return new SolidColorBrush(Color.FromRgb(0, 128, 255)); case Status.Success: return new SolidColorBrush(Color.FromRgb(0, 135, 60)); case Status.Working: return new SolidColorBrush(Color.FromRgb(226, 90, 0)); - case Status.Error: return new SolidColorBrush(Color.FromRgb(216, 14, 0)); + case Status.Error: return new SolidColorBrush(Color.FromRgb(216, 14, 0)); + case Status.Unauthenticated: return new SolidColorBrush(Color.FromRgb(120, 0, 120)); default: throw new Exception($"Invalid app status {_status.ToString()}"); } } diff --git a/RescuerLaApp/Models/Enums.cs b/RescuerLaApp/Models/Enums.cs index 1988484..99cfb3c 100755 --- a/RescuerLaApp/Models/Enums.cs +++ b/RescuerLaApp/Models/Enums.cs @@ -7,6 +7,7 @@ public enum Status Ready, Working, Success, + Unauthenticated, Error } diff --git a/RescuerLaApp/Models/PasswordHasher.cs b/RescuerLaApp/Models/PasswordHasher.cs new file mode 100644 index 0000000..b877b72 --- /dev/null +++ b/RescuerLaApp/Models/PasswordHasher.cs @@ -0,0 +1,83 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Cryptography.KeyDerivation; + +namespace RescuerLaApp.Models +{ + public class PasswordHasher + { + public string GenerateIdentityV3Hash(string password, KeyDerivationPrf prf = KeyDerivationPrf.HMACSHA256, int iterationCount = 10000, int saltSize = 16) + { + using (var rng = RandomNumberGenerator.Create()){ + var salt = new byte[saltSize]; + rng.GetBytes(salt); + + var pbkdf2Hash = KeyDerivation.Pbkdf2(password, salt, prf, iterationCount, 32); + return Convert.ToBase64String(ComposeIdentityV3Hash(salt, (uint)iterationCount, pbkdf2Hash)); + } + } + public bool VerifyIdentityV3Hash(string password, string passwordHash) + { + var identityV3HashArray = Convert.FromBase64String(passwordHash); + if (identityV3HashArray[0] != 1) throw new InvalidOperationException("passwordHash is not Identity V3"); + + var prfAsArray = new byte[4]; + Buffer.BlockCopy(identityV3HashArray, 1, prfAsArray, 0, 4); + var prf = (KeyDerivationPrf)ConvertFromNetworOrder(prfAsArray); + + var iterationCountAsArray = new byte[4]; + Buffer.BlockCopy(identityV3HashArray, 5, iterationCountAsArray, 0, 4); + var iterationCount = (int)ConvertFromNetworOrder(iterationCountAsArray); + + var saltSizeAsArray = new byte[4]; + Buffer.BlockCopy(identityV3HashArray, 9, saltSizeAsArray, 0, 4); + var saltSize = (int)ConvertFromNetworOrder(saltSizeAsArray); + + var salt = new byte[saltSize]; + Buffer.BlockCopy(identityV3HashArray, 13, salt, 0, saltSize); + + var savedHashedPassword = new byte[identityV3HashArray.Length - 1 - 4 - 4 - 4 - saltSize]; + Buffer.BlockCopy(identityV3HashArray, 13 + saltSize, savedHashedPassword, 0, savedHashedPassword.Length); + + var hashFromInputPassword = KeyDerivation.Pbkdf2(password, salt, prf, iterationCount, 32); + + return AreByteArraysEqual(hashFromInputPassword, savedHashedPassword); + } + private byte[] ComposeIdentityV3Hash(byte[] salt, uint iterationCount, byte[] passwordHash) + { + var hash = new byte[1 + 4/*KeyDerivationPrf value*/ + 4/*Iteration count*/ + 4/*salt size*/ + salt.Length /*salt*/ + 32 /*password hash size*/]; + hash[0] = 1; //Identity V3 marker + + Buffer.BlockCopy(ConvertToNetworkOrder((uint)KeyDerivationPrf.HMACSHA256), 0, hash, 1, sizeof(uint)); + Buffer.BlockCopy(ConvertToNetworkOrder((uint)iterationCount), 0, hash, 1 + sizeof(uint), sizeof(uint)); + Buffer.BlockCopy(ConvertToNetworkOrder((uint)salt.Length), 0, hash, 1 + 2 * sizeof(uint), sizeof(uint)); + Buffer.BlockCopy(salt, 0, hash, 1 + 3 * sizeof(uint), salt.Length); + Buffer.BlockCopy(passwordHash, 0, hash, 1 + 3 * sizeof(uint) + salt.Length, passwordHash.Length); + + return hash; + } + + private bool AreByteArraysEqual(byte[] array1, byte[] array2) + { + if (array1.Length != array2.Length) return false; + + var areEqual = true; + for (var i = 0; i < array1.Length; i++) + { + areEqual &= (array1[i] == array2[i]); + } + return areEqual; + } + + private byte[] ConvertToNetworkOrder(uint number) + { + return BitConverter.GetBytes(number).Reverse().ToArray(); + } + + private uint ConvertFromNetworOrder(byte[] reversedUint) + { + return BitConverter.ToUInt32(reversedUint.Reverse().ToArray(), 0); + } + } +} \ No newline at end of file diff --git a/RescuerLaApp/Program.cs b/RescuerLaApp/Program.cs index 9ae8e23..f57506b 100755 --- a/RescuerLaApp/Program.cs +++ b/RescuerLaApp/Program.cs @@ -11,7 +11,7 @@ internal static class Program { private static void Main(string[] args) { - Console.WriteLine("Lacmus desktop application. Version 0.3.0 alpha. \nCopyright (c) 2019 Georgy Perevozghikov \nGithub page: https://github.com/lizaalert/lacmus/.\nProvided by Yandex Cloud: https://cloud.yandex.com/."); + Console.WriteLine("Lacmus desktop application. Version 0.3.1 alpha. \nCopyright (c) 2019 Georgy Perevozghikov \nGithub page: https://github.com/lizaalert/lacmus/.\nProvided by Yandex Cloud: https://cloud.yandex.com/."); Console.WriteLine("This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'."); Console.WriteLine("This is free software, and you are welcome to redistribute it\nunder certain conditions; type `show c' for details."); Console.WriteLine("------------------------------------"); diff --git a/RescuerLaApp/RescuerLaApp.csproj b/RescuerLaApp/RescuerLaApp.csproj index fca7e35..f75fd5f 100755 --- a/RescuerLaApp/RescuerLaApp.csproj +++ b/RescuerLaApp/RescuerLaApp.csproj @@ -23,6 +23,7 @@ + diff --git a/RescuerLaApp/ViewModels/MainWindowViewModel.cs b/RescuerLaApp/ViewModels/MainWindowViewModel.cs index 4c7e306..0ce0857 100755 --- a/RescuerLaApp/ViewModels/MainWindowViewModel.cs +++ b/RescuerLaApp/ViewModels/MainWindowViewModel.cs @@ -20,6 +20,7 @@ using ReactiveUI; using ReactiveUI.Fody.Helpers; using Newtonsoft.Json; +using RescuerLaApp.Views; using Directory = System.IO.Directory; namespace RescuerLaApp.ViewModels @@ -48,7 +49,11 @@ public MainWindowViewModel() var canExecute = this .WhenAnyValue(x => x.Status) - .Select(status => status.Status != Enums.Status.Working); + .Select(status => status.Status != Enums.Status.Working && status.Status != Enums.Status.Unauthenticated); + + var canAuth = this + .WhenAnyValue(x => x.Status) + .Select(status => status.Status == Enums.Status.Unauthenticated); var canShowPedestrians = this .WhenAnyValue(x => x._frames) @@ -75,7 +80,9 @@ public MainWindowViewModel() ShowGeoDataCommand = ReactiveCommand.Create(ShowGeoData, canExecute); HelpCommand = ReactiveCommand.Create(Help); AboutCommand = ReactiveCommand.Create(About); - ExitCommand = ReactiveCommand.Create(Exit, canExecute); + SignUpCommand = ReactiveCommand.Create(SignUp, canAuth); + SignInCommand = ReactiveCommand.Create(SignIn, canAuth); + ExitCommand = ReactiveCommand.Create(Exit); } public void UpdateFramesRepo() @@ -106,7 +113,7 @@ public void UpdateFramesRepo() [Reactive] public List Frames { get; set; } = new List(); - [Reactive] public AppStatusInfo Status { get; set; } = new AppStatusInfo { Status = Enums.Status.Ready }; + [Reactive] public AppStatusInfo Status { get; set; } = new AppStatusInfo { Status = Enums.Status.Unauthenticated }; [Reactive] public ImageBrush ImageBrush { get; set; } = new ImageBrush { Stretch = Stretch.Uniform }; @@ -140,6 +147,8 @@ public void UpdateFramesRepo() public ReactiveCommand ShowGeoDataCommand { get; } public ReactiveCommand HelpCommand { get; } public ReactiveCommand AboutCommand { get; } + public ReactiveCommand SignUpCommand { get; } + public ReactiveCommand SignInCommand { get; } public ReactiveCommand ExitCommand { get; } @@ -625,7 +634,7 @@ public async void About() new ButtonDefinition{Name = "Github"} }, ContentTitle = "About", - ContentHeader = "Lacmus desktop application. Version 0.3.0 alpha.", + ContentHeader = "Lacmus desktop application. Version 0.3.1 alpha.", ContentMessage = message, Icon = Icon.Avalonia, Style = Style.None, @@ -640,6 +649,19 @@ public async void About() case "github": OpenUrl("https://github.com/lizaalert/lacmus"); break; } } + + public async void SignUp() + { + var result = await Views.SignUpWindow.Show(null); + if(Status.Status == Enums.Status.Unauthenticated && result.IsSignIn) + Status = new AppStatusInfo() {Status = Enums.Status.Ready}; + } + public async void SignIn() + { + var result = await Views.SignInWindow.Show(null); + if(Status.Status == Enums.Status.Unauthenticated && result.IsSignIn) + Status = new AppStatusInfo() {Status = Enums.Status.Ready}; + } public async void Exit() { diff --git a/RescuerLaApp/Views/MainWindow.xaml b/RescuerLaApp/Views/MainWindow.xaml index 193bd5c..4f3a7f0 100755 --- a/RescuerLaApp/Views/MainWindow.xaml +++ b/RescuerLaApp/Views/MainWindow.xaml @@ -25,6 +25,9 @@ + + + @@ -49,6 +52,7 @@ + diff --git a/RescuerLaApp/Views/SignInWindow.xaml b/RescuerLaApp/Views/SignInWindow.xaml new file mode 100644 index 0000000..17b1193 --- /dev/null +++ b/RescuerLaApp/Views/SignInWindow.xaml @@ -0,0 +1,20 @@ + + + Email + + Password + + + + + + + + \ No newline at end of file diff --git a/RescuerLaApp/Views/SignInWindow.xaml.cs b/RescuerLaApp/Views/SignInWindow.xaml.cs new file mode 100644 index 0000000..4a323df --- /dev/null +++ b/RescuerLaApp/Views/SignInWindow.xaml.cs @@ -0,0 +1,113 @@ +using System; +using System.Data; +using System.IO; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using MessageBox.Avalonia.DTO; +using MessageBox.Avalonia.Enums; +using Newtonsoft.Json; +using RescuerLaApp.Models; + +namespace RescuerLaApp.Views +{ + class SignInWindow : Window + { + [JsonObject] + public class SignInResult + { + [JsonProperty("email")] + public string Email { get; set; } + [JsonProperty("passwordHash")] + public string PasswordHash { get; set; } + [JsonProperty("id")] + public int Id { get; set; } + [JsonProperty("time")] + public string Time { get; set; } + + public bool IsSignIn { get; set; } = false; + } + + public SignInWindow() + { + AvaloniaXamlLoader.Load(this); + } + + public static Task Show(Window parent) + { + var title = "Sign In"; + var msgbox = new SignInWindow() + { + Title = title + }; + var buttonPanel = msgbox.FindControl("Buttons"); + + SignInResult res = new SignInResult(); + + void AddButton(string caption) + { + var btn = new Button {Content = caption}; + btn.Click += (_, __) => + { + var patch = AppDomain.CurrentDomain.BaseDirectory + "user_info"; + if (File.Exists(patch)) + { + res = JsonConvert.DeserializeObject(File.ReadAllText(patch)); + } + else + { + ShowError("There are no account. Please sign up"); + return; + } + + var passwordHash = msgbox.FindControl("tbPassword").Text; + var email = msgbox.FindControl("tbEmail").Text; + + if (string.IsNullOrWhiteSpace(passwordHash) || passwordHash.Length < 6) + { + ShowError("Incorrect Password"); + return; + } + PasswordHasher hasher = new PasswordHasher(); + if (string.IsNullOrWhiteSpace(passwordHash) || !hasher.VerifyIdentityV3Hash(passwordHash, res.PasswordHash)) + { + ShowError("Incorrect Password"); + return; + } + if (string.IsNullOrWhiteSpace(email) || !email.Contains('@') || !email.Contains('.') || email != res.Email) + { + ShowError("Incorrect Email"); + return; + } + + res.IsSignIn = true; + msgbox.Close(); + }; + buttonPanel.Children.Add(btn); + } + + AddButton("Sign Ip"); + var tcs = new TaskCompletionSource(); + msgbox.Closed += delegate { tcs.TrySetResult(res); }; + if (parent != null) + msgbox.ShowDialog(parent); + else msgbox.Show(); + return tcs.Task; + } + + private static async void ShowError(string message) + { + var msgbox = new MessageBox.Avalonia.MessageBoxWindow(new MessageBoxParams + { + Button = ButtonEnum.Ok, + ContentTitle = "Error", + ContentMessage = message, + Icon = MessageBox.Avalonia.Enums.Icon.Error, + Style = Style.None, + ShowInCenter = true + }); + var result = await msgbox.Show(); + } + } +} \ No newline at end of file diff --git a/RescuerLaApp/Views/SignUpWindow.Xaml b/RescuerLaApp/Views/SignUpWindow.Xaml new file mode 100644 index 0000000..e69de29 diff --git a/RescuerLaApp/Views/SignUpWindow.xaml b/RescuerLaApp/Views/SignUpWindow.xaml new file mode 100644 index 0000000..f338642 --- /dev/null +++ b/RescuerLaApp/Views/SignUpWindow.xaml @@ -0,0 +1,26 @@ + + + Email + + Nick name + + First name + + Last name + + Password + + + + + + + + \ No newline at end of file diff --git a/RescuerLaApp/Views/SignUpWindow.xaml.cs b/RescuerLaApp/Views/SignUpWindow.xaml.cs new file mode 100644 index 0000000..a70456b --- /dev/null +++ b/RescuerLaApp/Views/SignUpWindow.xaml.cs @@ -0,0 +1,149 @@ +using System; +using System.Data; +using System.IO; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using MessageBox.Avalonia.DTO; +using MessageBox.Avalonia.Enums; +using Newtonsoft.Json; +using RescuerLaApp.Models; + +namespace RescuerLaApp.Views +{ + class SignUpWindow : Window + { + [JsonObject] + public class SignUpResult + { + [JsonProperty("email")] + public string Email { get; set; } + [JsonProperty("passwordHash")] + public string PasswordHash { get; set; } + [JsonProperty("id")] + public int Id { get; set; } + [JsonProperty("time")] + public string Time { get; set; } + + public bool IsSignIn { get; set; } = false; + } + + public SignUpWindow() + { + AvaloniaXamlLoader.Load(this); + } + + public static Task Show(Window parent) + { + var title = "Create Account"; + var msgbox = new SignUpWindow() + { + Title = title + }; + var buttonPanel = msgbox.FindControl("Buttons"); + + SignUpResult res = new SignUpResult(); + + void AddButton(string caption) + { + var btn = new Button {Content = caption}; + btn.Click += (_, __) => + { + var nickName = msgbox.FindControl("tbNickName").Text; + var passwordHash = msgbox.FindControl("tbPassword").Text; + var email = msgbox.FindControl("tbEmail").Text; + var firstName = msgbox.FindControl("tbFirstName").Text; + var lastName = msgbox.FindControl("tbLastName").Text; + if (string.IsNullOrWhiteSpace(nickName)) + { + ShowError("Incorrect Nick name"); + return; + } + if (string.IsNullOrWhiteSpace(passwordHash) || passwordHash.Length < 6) + { + ShowError("Incorrect Password"); + return; + } + if (string.IsNullOrWhiteSpace(firstName)) + { + ShowError("Incorrect First name"); + return; + } + if (string.IsNullOrWhiteSpace(lastName)) + { + ShowError("Incorrect Last name"); + return; + } + if (string.IsNullOrWhiteSpace(email) || !email.Contains('@') || !email.Contains('.')) + { + ShowError("Incorrect Email"); + return; + } + + PasswordHasher hasher = new PasswordHasher(); + passwordHash = hasher.GenerateIdentityV3Hash(passwordHash); + + res = new SignUpResult + { + Email = email, + PasswordHash = passwordHash, + Id = 1, + Time = DateTime.Now.ToString() + }; + + var patch = AppDomain.CurrentDomain.BaseDirectory + "user_info"; + if (File.Exists(patch)) + { + ShowInfo("You already signed up. Please log in."); + msgbox.Close(); + return; + } + + res.IsSignIn = true; + + File.AppendAllText( + patch, + JsonConvert.SerializeObject(res)); + msgbox.Close(); + }; + buttonPanel.Children.Add(btn); + } + + AddButton("Sign Up"); + var tcs = new TaskCompletionSource(); + msgbox.Closed += delegate { tcs.TrySetResult(res); }; + if (parent != null) + msgbox.ShowDialog(parent); + else msgbox.Show(); + return tcs.Task; + } + + private static async void ShowError(string message) + { + var msgbox = new MessageBox.Avalonia.MessageBoxWindow(new MessageBoxParams + { + Button = ButtonEnum.Ok, + ContentTitle = "Error", + ContentMessage = message, + Icon = MessageBox.Avalonia.Enums.Icon.Error, + Style = Style.None, + ShowInCenter = true + }); + var result = await msgbox.Show(); + } + private static async void ShowInfo(string message) + { + var msgbox = new MessageBox.Avalonia.MessageBoxWindow(new MessageBoxParams + { + Button = ButtonEnum.Ok, + ContentTitle = "Info", + ContentMessage = message, + Icon = MessageBox.Avalonia.Enums.Icon.Lock, + Style = Style.None, + ShowInCenter = true + }); + var result = await msgbox.Show(); + } + } +} \ No newline at end of file