From 8a4a0f83561fba7c1011ded63922b199db880ce4 Mon Sep 17 00:00:00 2001 From: Vincenzo Ciaccio Date: Mon, 7 Nov 2022 20:54:14 +0000 Subject: [PATCH] Adding Username/Email to Password Entry (#8) --- Muscurdi.Tests/Models/MasterPasswordTests.cs | 10 +- Muscurdi/Assets/Styles/Main.xaml | 164 +++++++++++++--- .../Exceptions/InvalidPasswordException.cs | 2 +- Muscurdi/Libs/Crypto.cs | 2 +- Muscurdi/Libs/Singleton.cs | 2 +- Muscurdi/Models/Key.cs | 4 +- Muscurdi/Models/PasswordEntry.cs | 7 +- Muscurdi/Program.cs | 1 + Muscurdi/Services/Db.cs | 26 ++- .../{AddViewModels.cs => AddViewModel.cs} | 48 ++++- .../{ListViewModels.cs => ListViewModel.cs} | 25 ++- Muscurdi/Views/AddView.axaml | 80 +++++--- Muscurdi/Views/AddView.axaml.cs | 2 +- Muscurdi/Views/ListView.axaml | 183 ++++++++++++------ Muscurdi/Views/ListView.axaml.cs | 2 +- Muscurdi/Views/MainWindow.axaml.cs | 2 +- Muscurdi/Views/SplashView.axaml | 103 +++++----- 17 files changed, 466 insertions(+), 197 deletions(-) rename Muscurdi/ViewModels/{AddViewModels.cs => AddViewModel.cs} (57%) rename Muscurdi/ViewModels/{ListViewModels.cs => ListViewModel.cs} (75%) diff --git a/Muscurdi.Tests/Models/MasterPasswordTests.cs b/Muscurdi.Tests/Models/MasterPasswordTests.cs index 0a6dfef..e15effe 100644 --- a/Muscurdi.Tests/Models/MasterPasswordTests.cs +++ b/Muscurdi.Tests/Models/MasterPasswordTests.cs @@ -44,15 +44,15 @@ public void MasterPasswordMakeFromBitsHappyPath() var password = MasterPassword.Make(new() { "nope", "yeah", "yeah" }, "mamm", 9999); Assert.Equal("nope-yeah-yeah-mamm-9999", password.ToMemorable()); } - - + + [Fact] public void MasterPasswordMakeFromBitsFailsWithNotEnoughBits() { var result = Assert.Throws(() => MasterPassword.Make(new() { "nope", "yeah" }, "mamm", 9999)); Assert.Contains("prefix", result.Message); } - + [Fact] public void MasterPasswordMakeFromBitsFailsWithWordsNotLongEnough() { @@ -73,14 +73,14 @@ public void MasterPasswordMakeFromBitsFailsWithFinalTooLong() var result = Assert.Throws(() => MasterPassword.Make(new() { "nope", "yeah", "yoyo" }, "mammamia", 9999)); Assert.Contains("words", result.Message); } - + [Fact] public void MasterPasswordMakeFromBitsFailsWithAppendixTooBig() { var result = Assert.Throws(() => MasterPassword.Make(new() { "nope", "yeah", "yoyo" }, "mammamia", 10000)); Assert.Contains("appendix", result.Message); } - + [Fact] public void MasterPasswordMakeFromBitsFailsWithAppendixTooShort() { diff --git a/Muscurdi/Assets/Styles/Main.xaml b/Muscurdi/Assets/Styles/Main.xaml index 3f087f9..e22ebbe 100644 --- a/Muscurdi/Assets/Styles/Main.xaml +++ b/Muscurdi/Assets/Styles/Main.xaml @@ -1,59 +1,159 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Muscurdi/Exceptions/InvalidPasswordException.cs b/Muscurdi/Exceptions/InvalidPasswordException.cs index 2bfd017..237d6d3 100644 --- a/Muscurdi/Exceptions/InvalidPasswordException.cs +++ b/Muscurdi/Exceptions/InvalidPasswordException.cs @@ -1,5 +1,5 @@ namespace Muscurdi.Exceptions; -public class InvalidPasswordException: DbException +public class InvalidPasswordException : DbException { public InvalidPasswordException(string? message) : base(message) { } } diff --git a/Muscurdi/Libs/Crypto.cs b/Muscurdi/Libs/Crypto.cs index afc11bc..244ade0 100644 --- a/Muscurdi/Libs/Crypto.cs +++ b/Muscurdi/Libs/Crypto.cs @@ -60,7 +60,7 @@ public static string Encrypt(string plainText, MasterPassword password) } } } - catch (Exception exc) + catch (Exception) { return null; } diff --git a/Muscurdi/Libs/Singleton.cs b/Muscurdi/Libs/Singleton.cs index 795a5d8..8a79ac7 100644 --- a/Muscurdi/Libs/Singleton.cs +++ b/Muscurdi/Libs/Singleton.cs @@ -2,7 +2,7 @@ namespace Muscurdi.Libs; public class Singleton where T : class, new() { - private static volatile T _instance; + private static volatile T? _instance; private static readonly object _lock = new object(); public static T Instance { diff --git a/Muscurdi/Models/Key.cs b/Muscurdi/Models/Key.cs index 69456f3..f2afaf5 100644 --- a/Muscurdi/Models/Key.cs +++ b/Muscurdi/Models/Key.cs @@ -1,6 +1,6 @@ namespace Muscurdi.Models; public class Key { - public string Id { get; set; } - public string Value { get; set; } + public string? Id { get; set; } + public string? Value { get; set; } } diff --git a/Muscurdi/Models/PasswordEntry.cs b/Muscurdi/Models/PasswordEntry.cs index 962b1db..7cac435 100644 --- a/Muscurdi/Models/PasswordEntry.cs +++ b/Muscurdi/Models/PasswordEntry.cs @@ -2,7 +2,8 @@ namespace Muscurdi.Models; public class PasswordEntry { [LiteDB.BsonId] - public LiteDB.ObjectId Id { get; set; } - public string Name { get; set; } - public string Password { get; set; } + public LiteDB.ObjectId? Id { get; set; } + public string? Name { get; set; } + public string? Username { get; set; } + public string? Password { get; set; } } diff --git a/Muscurdi/Program.cs b/Muscurdi/Program.cs index 6f2e078..2e4f4d4 100644 --- a/Muscurdi/Program.cs +++ b/Muscurdi/Program.cs @@ -11,6 +11,7 @@ class Program [STAThread] public static void Main(string[] args) { + // maybe we can disable this on debug? SingleAppInstance.Check("muscurd-i"); BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); diff --git a/Muscurdi/Services/Db.cs b/Muscurdi/Services/Db.cs index 6d625ba..67d07c8 100644 --- a/Muscurdi/Services/Db.cs +++ b/Muscurdi/Services/Db.cs @@ -45,7 +45,7 @@ public void Init(MasterPassword masterPassword) { _key = (Crypto.Decrypt(key.Value, masterPassword) == masterPassword.ToString()) ? masterPassword : null; } - catch (Exception exc) + catch (Exception) { throw new InvalidPasswordException("Wrong Master Password"); } @@ -56,6 +56,28 @@ public void Init(MasterPassword masterPassword) Passwords = _db.GetCollection(PASSWORD_ENTRIES_COLLECTION); } + public int Count() => Passwords.Count(); + + public PasswordEntry? GetById(LiteDB.ObjectId id) + { + var password = Passwords.FindById(id); + password.Password = Crypto.Decrypt(password.Password, _key); + return password; + } + + public List? GetAll() => Passwords.FindAll().Select(p => + { + p.Password = Crypto.Decrypt(p.Password, _key) ?? "ERROR-DECRYPTING"; + return p; + }).ToList(); + + + public void UpdatePassword(PasswordEntry password) + { + password.Password = Crypto.Encrypt(password.Password, _key); + Passwords.Update(password); + } + public bool AddPassword(PasswordEntry password) { Passwords.EnsureIndex(x => x.Name, true); @@ -65,7 +87,7 @@ public bool AddPassword(PasswordEntry password) { result = Passwords.Insert(password); } - catch (LiteDB.LiteException _) + catch (LiteDB.LiteException) { result = null; diff --git a/Muscurdi/ViewModels/AddViewModels.cs b/Muscurdi/ViewModels/AddViewModel.cs similarity index 57% rename from Muscurdi/ViewModels/AddViewModels.cs rename to Muscurdi/ViewModels/AddViewModel.cs index fab29c3..7c75612 100644 --- a/Muscurdi/ViewModels/AddViewModels.cs +++ b/Muscurdi/ViewModels/AddViewModel.cs @@ -4,23 +4,27 @@ namespace Muscurdi.ViewModels; using Avalonia.Threading; using System.Reactive; using Muscurdi.Services; - +using Muscurdi.Models; public class AddViewModel : ReactiveObject, IRoutableViewModel { public IScreen HostScreen { get; } public string UrlPathSegment { get; } = "Add"; public ReactiveCommand Back { get; } - + private PasswordEntry? _existingRecord = null; private string _name = string.Empty; + + public string PageTitle { get; } = "Add New Password"; public string Name { get => _name; set => this.RaiseAndSetIfChanged(ref _name, value); } + private string _username = string.Empty; + public string Username { get => _username; set => this.RaiseAndSetIfChanged(ref _username, value); } private string _password = string.Empty; public string Password { get => _password; set => this.RaiseAndSetIfChanged(ref _password, value); } private string? _passChar = "*"; public string? PassChar { get => _passChar; set => this.RaiseAndSetIfChanged(ref _passChar, value); } - private string _showPassIcon = "fa fa-eye"; + private string _showPassIcon = "fa-eye"; public string ShowPassIcon { get => _showPassIcon; set => this.RaiseAndSetIfChanged(ref _showPassIcon, value); } @@ -30,31 +34,59 @@ public class AddViewModel : ReactiveObject, IRoutableViewModel private string? _error = null; public string? Error { get => _error; set => this.RaiseAndSetIfChanged(ref _error, value); } - public AddViewModel(IScreen screen) + public AddViewModel(IScreen screen, LiteDB.ObjectId? id = null) { HostScreen = screen; Back = HostScreen.Router.NavigateBack; + if (id != null) + { + _existingRecord = S.Instance.Db.GetById(id); + Name = _existingRecord.Name; + Username = _existingRecord.Username; + Password = _existingRecord.Password; + PageTitle = "Update Password"; + } + Add = ReactiveCommand.Create(() => { - var result = S.Instance.Db.AddPassword(new() { Name = Name, Password = Password }); - if (!result) + if (_existingRecord != null) + { + updatePassword(); + HostScreen.Router.NavigateAndReset.Execute(new ListViewModel(this.HostScreen)); + return; + } + + + if (!addNewPassword()) { Error = $"Can't add {Name}, probably exists already"; DispatcherTimer.RunOnce( () => Error = null, System.TimeSpan.FromSeconds(3) ); - return; } + Name = string.Empty; + Username = string.Empty; Password = string.Empty; HostScreen.Router.NavigateAndReset.Execute(new ListViewModel(this.HostScreen)); }); ShowPass = ReactiveCommand.Create(() => { PassChar = PassChar == "*" ? null : "*"; - ShowPassIcon = PassChar == null ? "fa fa-eye-slash" : "fa fa-eye"; + ShowPassIcon = PassChar == null ? "fa-eye-slash" : "fa-eye"; }); } + + private void updatePassword() + { + _existingRecord.Name = Name; + _existingRecord.Username = Username; + _existingRecord.Password = Password; + S.Instance.Db.UpdatePassword(_existingRecord); + } + + private bool addNewPassword() => S.Instance.Db.AddPassword(new() { Name = Name, Username = Username, Password = Password }); + } \ No newline at end of file diff --git a/Muscurdi/ViewModels/ListViewModels.cs b/Muscurdi/ViewModels/ListViewModel.cs similarity index 75% rename from Muscurdi/ViewModels/ListViewModels.cs rename to Muscurdi/ViewModels/ListViewModel.cs index fd091d7..3b20fd4 100644 --- a/Muscurdi/ViewModels/ListViewModels.cs +++ b/Muscurdi/ViewModels/ListViewModel.cs @@ -17,32 +17,38 @@ public class ListViewModel : ReactiveObject, IRoutableViewModel public ReactiveCommand CopyToClipboard { get; set; } public ReactiveCommand DeletePassword { get; set; } + public ReactiveCommand UpdateEntry { get; set; } private ObservableCollection _passwords = new(); public ObservableCollection Passwords { get => _passwords; set => this.RaiseAndSetIfChanged(ref _passwords, value); } private string? _searchText = null; public string? SearchText { get => _searchText; set => this.RaiseAndSetIfChanged(ref _searchText, value); } - private string? _searchStatus = "Here there will be your passwords..."; + private string? _searchStatus = null; public string? SearchStatus { get => _searchStatus; set => this.RaiseAndSetIfChanged(ref _searchStatus, value); } private string? _message = null; public string? Message { get => _message; set => this.RaiseAndSetIfChanged(ref _message, value); } public ReactiveCommand Search { get; set; } + public ReactiveCommand ShowAll { get; set; } public ListViewModel(IScreen screen) { HostScreen = screen; GoToAdd = ReactiveCommand.CreateFromObservable(() => HostScreen.Router.Navigate.Execute(new AddViewModel(this.HostScreen))); - CopyToClipboard = ReactiveCommand.Create((string password) => + SearchStatus = $"{S.Instance.Db.Count()} Passwords Saved."; + + CopyToClipboard = ReactiveCommand.Create((string text) => { - Avalonia.Application.Current?.Clipboard?.SetTextAsync(password); + Avalonia.Application.Current?.Clipboard?.SetTextAsync(text); Message = "Copied to Clipboard!"; DispatcherTimer.RunOnce( () => Message = null, System.TimeSpan.FromSeconds(3) ); }); + + UpdateEntry = ReactiveCommand.CreateFromObservable((LiteDB.ObjectId id) => HostScreen.Router.Navigate.Execute(new AddViewModel(this.HostScreen, id))); DeletePassword = ReactiveCommand.Create((LiteDB.ObjectId id) => { if (S.Instance.Db.Passwords.Delete(id)) @@ -69,5 +75,18 @@ public ListViewModel(IScreen screen) } }, canSearch); + ShowAll = ReactiveCommand.Create(() => + { + Passwords = new(S.Instance.Db.GetAll()); + if (Passwords.Count == 0) + { + SearchStatus = "No Passwords Stored yet..."; + } + else + { + SearchStatus = null; + } + }); + } } \ No newline at end of file diff --git a/Muscurdi/Views/AddView.axaml b/Muscurdi/Views/AddView.axaml index 83004f3..d3f752c 100644 --- a/Muscurdi/Views/AddView.axaml +++ b/Muscurdi/Views/AddView.axaml @@ -1,46 +1,62 @@ + 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" + xmlns:i="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" + x:Class="Muscurdi.Views.AddView"> - - - - + + + + - - - - + + + - - + - - - + + diff --git a/Muscurdi/Views/AddView.axaml.cs b/Muscurdi/Views/AddView.axaml.cs index 228d999..02accff 100644 --- a/Muscurdi/Views/AddView.axaml.cs +++ b/Muscurdi/Views/AddView.axaml.cs @@ -8,7 +8,7 @@ public partial class AddView : ReactiveUserControl { public AddView() { - this.WhenActivated(disposables => {}); + this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); } } diff --git a/Muscurdi/Views/ListView.axaml b/Muscurdi/Views/ListView.axaml index edc9835..6cbff0c 100644 --- a/Muscurdi/Views/ListView.axaml +++ b/Muscurdi/Views/ListView.axaml @@ -1,75 +1,146 @@ + 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" + xmlns:i="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" + x:Class="Muscurdi.Views.ListView"> - - - - + + + + - - + + + + + + + - + - + + + + + + Watermark="Search Password" + Width="200" + MaxWidth="200" + FontSize="14" /> - - - - - - + + + - - - - + + + + + + - - - + diff --git a/Muscurdi/Views/ListView.axaml.cs b/Muscurdi/Views/ListView.axaml.cs index d401581..48c3052 100644 --- a/Muscurdi/Views/ListView.axaml.cs +++ b/Muscurdi/Views/ListView.axaml.cs @@ -8,7 +8,7 @@ public partial class ListView : ReactiveUserControl { public ListView() { - this.WhenActivated(disposables => {}); + this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); } } diff --git a/Muscurdi/Views/MainWindow.axaml.cs b/Muscurdi/Views/MainWindow.axaml.cs index cd742f3..f1d224c 100644 --- a/Muscurdi/Views/MainWindow.axaml.cs +++ b/Muscurdi/Views/MainWindow.axaml.cs @@ -10,7 +10,7 @@ public partial class MainWindow : ReactiveWindow { public MainWindow() { - this.WhenActivated(disposables => {}); + this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); } } \ No newline at end of file diff --git a/Muscurdi/Views/SplashView.axaml b/Muscurdi/Views/SplashView.axaml index d55e8de..526b7a6 100644 --- a/Muscurdi/Views/SplashView.axaml +++ b/Muscurdi/Views/SplashView.axaml @@ -1,62 +1,69 @@ + 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="Muscurdi.Views.SplashView"> - + - - + + - - - - + + + + - - - + + + + Classes="HCent HCCent VCCent" + Command="{Binding GeneratePassword}">Generate + Watermark="Master Password" + Classes="VCent HCent" + Mask="LLLL-LLLL-LLLL-LLLL-0000" + Text="{Binding Password}" + Width="205" + MaxWidth="205" + FontSize="14" + Margin="0,5,0,0" /> - - + Command="{Binding Login}" + IsDefault="True" + Classes="HCent VCent HCCent VCCent" + Margin="0,3,0,0">Login + +