From afa52253aaeeff30107be11a3f9f86a272819b3c Mon Sep 17 00:00:00 2001 From: Yassin Lokhat Date: Fri, 12 Jun 2026 12:16:19 +0300 Subject: [PATCH 01/19] Initial commit --- .gitignore | 33 + Core/Models/Account.cs | 199 ++++ Core/Models/Activity.cs | 114 ++ Core/Models/AutoSave.cs | 203 ++++ Core/Models/Change.cs | 14 + Core/Models/Database.cs | 541 +++++++++ Core/Models/Service.cs | 162 +++ Core/Models/User.cs | 319 +++++ Core/Models/Warning.cs | 26 + Core/Upsilon.Apps.Passkey.Core.csproj | 28 + Core/Utils/ActivityCenter.cs | 73 ++ Core/Utils/CryptographyCenter.cs | 274 +++++ Core/Utils/FileLocker.cs | 180 +++ Core/Utils/ImportExportHelper.cs | 199 ++++ Core/Utils/JsonSerializationCenter.cs | 17 + Core/Utils/PasswordFactory.cs | 54 + Core/Utils/QrCode.cs | 1041 +++++++++++++++++ Core/Utils/StaticMethods.cs | 26 + GUI/WPF/App.xaml | 13 + GUI/WPF/App.xaml.cs | 12 + GUI/WPF/AssemblyInfo.cs | 10 + GUI/WPF/Helper/EnumHelper.cs | 74 ++ GUI/WPF/Helper/HotKeyHelper.cs | 92 ++ GUI/WPF/Helper/IItemHelper.cs | 57 + GUI/WPF/Helper/NumericTextBoxHelper.cs | 57 + GUI/WPF/Helper/PropertyHelper.cs | 25 + GUI/WPF/Helper/WindowHelper.cs | 55 + GUI/WPF/MainWindow.xaml | 72 ++ GUI/WPF/MainWindow.xaml.cs | 215 ++++ GUI/WPF/OSSpecific/ClipboardManager.cs | 32 + .../PublishProfiles/FolderProfile.pubxml | 17 + GUI/WPF/Properties/launchSettings.json | 9 + GUI/WPF/Themes/DarkMode.cs | 31 + GUI/WPF/Themes/DarkTheme.xaml | 142 +++ GUI/WPF/Upsilon.Apps.Passkey.GUI.WPF.csproj | 42 + .../AccountPasswordsWarningViewModel.cs | 77 ++ .../AccountPasswordWarningViewModel.cs | 23 + .../ViewModels/Controls/AccountViewModel.cs | 247 ++++ .../ViewModels/Controls/ActivityViewModel.cs | 46 + .../DuplicatedPasswordWarningViewModel.cs | 18 + .../Controls/IdentifiantViewModel.cs | 77 ++ .../ViewModels/Controls/PasswordViewModel.cs | 17 + .../ViewModels/Controls/ServiceViewModel.cs | 126 ++ .../Controls/UserPasswordItemViewModel.cs | 26 + .../Controls/VisiblePasswordBoxViewModel.cs | 67 ++ .../DuplicatedPasswordsWarningViewModel.cs | 22 + .../ViewModels/InsertIdentifierViewModel.cs | 46 + GUI/WPF/ViewModels/MainViewModel.cs | 60 + .../ViewModels/PasswordGeneratorViewModel.cs | 168 +++ GUI/WPF/ViewModels/UserActivitiesViewModel.cs | 140 +++ GUI/WPF/ViewModels/UserServicesViewModel.cs | 200 ++++ GUI/WPF/ViewModels/UserSettingsViewModel.cs | 196 ++++ .../Views/AccountPasswordsWarningView.xaml | 96 ++ .../Views/AccountPasswordsWarningView.xaml.cs | 44 + GUI/WPF/Views/Controls/AccountView.xaml | 196 ++++ GUI/WPF/Views/Controls/AccountView.xaml.cs | 291 +++++ GUI/WPF/Views/Controls/ServiceView.xaml | 107 ++ GUI/WPF/Views/Controls/ServiceView.xaml.cs | 137 +++ GUI/WPF/Views/Controls/UserPasswordItem.xaml | 33 + .../Views/Controls/UserPasswordItem.xaml.cs | 70 ++ .../Controls/UserPasswordsContainer.xaml | 26 + .../Controls/UserPasswordsContainer.xaml.cs | 117 ++ .../Views/Controls/VisiblePasswordBox.xaml | 36 + .../Views/Controls/VisiblePasswordBox.xaml.cs | 99 ++ .../Views/DuplicatedPasswordsWarningView.xaml | 72 ++ .../DuplicatedPasswordsWarningView.xaml.cs | 42 + GUI/WPF/Views/InsertIdentifierView.xaml | 46 + GUI/WPF/Views/InsertIdentifierView.xaml.cs | 85 ++ GUI/WPF/Views/PasswordGeneratorView.xaml | 82 ++ GUI/WPF/Views/PasswordGeneratorView.xaml.cs | 72 ++ GUI/WPF/Views/QrCodeView.xaml | 14 + GUI/WPF/Views/QrCodeView.xaml.cs | 101 ++ GUI/WPF/Views/UserActivitiesView.xaml | 121 ++ GUI/WPF/Views/UserActivitiesView.xaml.cs | 44 + GUI/WPF/Views/UserServicesView.xaml | 155 +++ GUI/WPF/Views/UserServicesView.xaml.cs | 406 +++++++ GUI/WPF/Views/UserSettingsView.xaml | 122 ++ GUI/WPF/Views/UserSettingsView.xaml.cs | 358 ++++++ GUI/WPF/icon.ico | Bin 0 -> 165597 bytes ...N9TymqpQ==UGOcrUlBjHsiPMUpOVwOKjiSUBw=.pku | Bin 0 -> 1620038 bytes Interfaces/Enums/AccountOption.cs | 22 + Interfaces/Enums/ActivityEventType.cs | 103 ++ Interfaces/Enums/AutoSaveMergeBehavior.cs | 25 + Interfaces/Enums/WarningType.cs | 27 + .../Events/AutoSaveDetectedEventArgs.cs | 16 + Interfaces/Events/LogoutEventArgs.cs | 11 + Interfaces/Events/WarningsUpdatedEventArgs.cs | 16 + Interfaces/Models/IAccount.cs | 51 + Interfaces/Models/IActivity.cs | 35 + Interfaces/Models/IDatabase.cs | 129 ++ Interfaces/Models/IItem.cs | 24 + Interfaces/Models/IService.cs | 71 ++ Interfaces/Models/IUser.cs | 68 ++ Interfaces/Models/IWarning.cs | 25 + .../Upsilon.Apps.Passkey.Interfaces.csproj | 22 + Interfaces/Utils/Exceptions.cs | 36 + Interfaces/Utils/IClipboardManager.cs | 15 + Interfaces/Utils/ICryptographyCenter.cs | 79 ++ Interfaces/Utils/IPasswordFactory.cs | 41 + Interfaces/Utils/ISerializationCenter.cs | 24 + Interfaces/Utils/StaticMethods.cs | 12 + Upsilon.Apps.Passkey.Windows.slnx | 5 + 102 files changed, 9843 insertions(+) create mode 100644 .gitignore create mode 100644 Core/Models/Account.cs create mode 100644 Core/Models/Activity.cs create mode 100644 Core/Models/AutoSave.cs create mode 100644 Core/Models/Change.cs create mode 100644 Core/Models/Database.cs create mode 100644 Core/Models/Service.cs create mode 100644 Core/Models/User.cs create mode 100644 Core/Models/Warning.cs create mode 100644 Core/Upsilon.Apps.Passkey.Core.csproj create mode 100644 Core/Utils/ActivityCenter.cs create mode 100644 Core/Utils/CryptographyCenter.cs create mode 100644 Core/Utils/FileLocker.cs create mode 100644 Core/Utils/ImportExportHelper.cs create mode 100644 Core/Utils/JsonSerializationCenter.cs create mode 100644 Core/Utils/PasswordFactory.cs create mode 100644 Core/Utils/QrCode.cs create mode 100644 Core/Utils/StaticMethods.cs create mode 100644 GUI/WPF/App.xaml create mode 100644 GUI/WPF/App.xaml.cs create mode 100644 GUI/WPF/AssemblyInfo.cs create mode 100644 GUI/WPF/Helper/EnumHelper.cs create mode 100644 GUI/WPF/Helper/HotKeyHelper.cs create mode 100644 GUI/WPF/Helper/IItemHelper.cs create mode 100644 GUI/WPF/Helper/NumericTextBoxHelper.cs create mode 100644 GUI/WPF/Helper/PropertyHelper.cs create mode 100644 GUI/WPF/Helper/WindowHelper.cs create mode 100644 GUI/WPF/MainWindow.xaml create mode 100644 GUI/WPF/MainWindow.xaml.cs create mode 100644 GUI/WPF/OSSpecific/ClipboardManager.cs create mode 100644 GUI/WPF/Properties/PublishProfiles/FolderProfile.pubxml create mode 100644 GUI/WPF/Properties/launchSettings.json create mode 100644 GUI/WPF/Themes/DarkMode.cs create mode 100644 GUI/WPF/Themes/DarkTheme.xaml create mode 100644 GUI/WPF/Upsilon.Apps.Passkey.GUI.WPF.csproj create mode 100644 GUI/WPF/ViewModels/AccountPasswordsWarningViewModel.cs create mode 100644 GUI/WPF/ViewModels/Controls/AccountPasswordWarningViewModel.cs create mode 100644 GUI/WPF/ViewModels/Controls/AccountViewModel.cs create mode 100644 GUI/WPF/ViewModels/Controls/ActivityViewModel.cs create mode 100644 GUI/WPF/ViewModels/Controls/DuplicatedPasswordWarningViewModel.cs create mode 100644 GUI/WPF/ViewModels/Controls/IdentifiantViewModel.cs create mode 100644 GUI/WPF/ViewModels/Controls/PasswordViewModel.cs create mode 100644 GUI/WPF/ViewModels/Controls/ServiceViewModel.cs create mode 100644 GUI/WPF/ViewModels/Controls/UserPasswordItemViewModel.cs create mode 100644 GUI/WPF/ViewModels/Controls/VisiblePasswordBoxViewModel.cs create mode 100644 GUI/WPF/ViewModels/DuplicatedPasswordsWarningViewModel.cs create mode 100644 GUI/WPF/ViewModels/InsertIdentifierViewModel.cs create mode 100644 GUI/WPF/ViewModels/MainViewModel.cs create mode 100644 GUI/WPF/ViewModels/PasswordGeneratorViewModel.cs create mode 100644 GUI/WPF/ViewModels/UserActivitiesViewModel.cs create mode 100644 GUI/WPF/ViewModels/UserServicesViewModel.cs create mode 100644 GUI/WPF/ViewModels/UserSettingsViewModel.cs create mode 100644 GUI/WPF/Views/AccountPasswordsWarningView.xaml create mode 100644 GUI/WPF/Views/AccountPasswordsWarningView.xaml.cs create mode 100644 GUI/WPF/Views/Controls/AccountView.xaml create mode 100644 GUI/WPF/Views/Controls/AccountView.xaml.cs create mode 100644 GUI/WPF/Views/Controls/ServiceView.xaml create mode 100644 GUI/WPF/Views/Controls/ServiceView.xaml.cs create mode 100644 GUI/WPF/Views/Controls/UserPasswordItem.xaml create mode 100644 GUI/WPF/Views/Controls/UserPasswordItem.xaml.cs create mode 100644 GUI/WPF/Views/Controls/UserPasswordsContainer.xaml create mode 100644 GUI/WPF/Views/Controls/UserPasswordsContainer.xaml.cs create mode 100644 GUI/WPF/Views/Controls/VisiblePasswordBox.xaml create mode 100644 GUI/WPF/Views/Controls/VisiblePasswordBox.xaml.cs create mode 100644 GUI/WPF/Views/DuplicatedPasswordsWarningView.xaml create mode 100644 GUI/WPF/Views/DuplicatedPasswordsWarningView.xaml.cs create mode 100644 GUI/WPF/Views/InsertIdentifierView.xaml create mode 100644 GUI/WPF/Views/InsertIdentifierView.xaml.cs create mode 100644 GUI/WPF/Views/PasswordGeneratorView.xaml create mode 100644 GUI/WPF/Views/PasswordGeneratorView.xaml.cs create mode 100644 GUI/WPF/Views/QrCodeView.xaml create mode 100644 GUI/WPF/Views/QrCodeView.xaml.cs create mode 100644 GUI/WPF/Views/UserActivitiesView.xaml create mode 100644 GUI/WPF/Views/UserActivitiesView.xaml.cs create mode 100644 GUI/WPF/Views/UserServicesView.xaml create mode 100644 GUI/WPF/Views/UserServicesView.xaml.cs create mode 100644 GUI/WPF/Views/UserSettingsView.xaml create mode 100644 GUI/WPF/Views/UserSettingsView.xaml.cs create mode 100644 GUI/WPF/icon.ico create mode 100644 GUI/WPF/raw/vidpll-sVqi93MN9TymqpQ==UGOcrUlBjHsiPMUpOVwOKjiSUBw=.pku create mode 100644 Interfaces/Enums/AccountOption.cs create mode 100644 Interfaces/Enums/ActivityEventType.cs create mode 100644 Interfaces/Enums/AutoSaveMergeBehavior.cs create mode 100644 Interfaces/Enums/WarningType.cs create mode 100644 Interfaces/Events/AutoSaveDetectedEventArgs.cs create mode 100644 Interfaces/Events/LogoutEventArgs.cs create mode 100644 Interfaces/Events/WarningsUpdatedEventArgs.cs create mode 100644 Interfaces/Models/IAccount.cs create mode 100644 Interfaces/Models/IActivity.cs create mode 100644 Interfaces/Models/IDatabase.cs create mode 100644 Interfaces/Models/IItem.cs create mode 100644 Interfaces/Models/IService.cs create mode 100644 Interfaces/Models/IUser.cs create mode 100644 Interfaces/Models/IWarning.cs create mode 100644 Interfaces/Upsilon.Apps.Passkey.Interfaces.csproj create mode 100644 Interfaces/Utils/Exceptions.cs create mode 100644 Interfaces/Utils/IClipboardManager.cs create mode 100644 Interfaces/Utils/ICryptographyCenter.cs create mode 100644 Interfaces/Utils/IPasswordFactory.cs create mode 100644 Interfaces/Utils/ISerializationCenter.cs create mode 100644 Interfaces/Utils/StaticMethods.cs create mode 100644 Upsilon.Apps.Passkey.Windows.slnx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d55aa05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ + +#Ignore thumbnails created by Windows +Thumbs.db +#Ignore files built by Visual Studio +*.obj +*.exe +*.pdb +*.user +*.aps +*.pch +*.vspscc +*_i.c +*_p.c +*.ncb +*.suo +*.tlb +*.tlh +*.bak +*.cache +*.ilk +*.log +[Bb]in +[Dd]ebug*/ +*.lib +*.sbr +obj/ +[Rr]elease*/ +_ReSharper*/ +[Tt]est[Rr]esult* +.vs/ +#Nuget packages folder +packages/ +build \ No newline at end of file diff --git a/Core/Models/Account.cs b/Core/Models/Account.cs new file mode 100644 index 0000000..dab658f --- /dev/null +++ b/Core/Models/Account.cs @@ -0,0 +1,199 @@ +using System.ComponentModel; +using Upsilon.Apps.Passkey.Core.Utils; +using Upsilon.Apps.Passkey.Interfaces.Enums; +using Upsilon.Apps.Passkey.Interfaces.Models; + +namespace Upsilon.Apps.Passkey.Core.Models +{ + internal sealed class Account : IAccount + { + #region IAccount interface explicit Internal + + string IItem.ItemId => Database.Get(ItemId); + + IDatabase IItem.Database => Database; + + IService IAccount.Service => Database.Get(Service); + + string IAccount.Label + { + get => Database.Get(Label); + set => Label = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(Label), + needsReview: false, + oldValue: Label, + newValue: value, + readableValue: value); + } + + string[] IAccount.Identifiers + { + get => Database.Get(Identifiers); + set => Identifiers = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(Identifiers), + needsReview: true, + oldValue: Identifiers, + newValue: value, + readableValue: $"({string.Join(", ", value)})"); + } + + string IAccount.Password + { + get => Database.Get(Password); + set + { + if (!string.IsNullOrEmpty(value) + && Password != value) + { + Dictionary oldPasswords = Passwords.CloneWith(Database.SerializationCenter); + Passwords[DateTime.Now] = Password = value; + + if (_service is not null) + { + if (Service.User.NumberOfOldPasswordToKeep != 0) + { + DateTime[] datesToRemove = [.. Passwords.Keys + .OrderBy(x => x) + .Take(Passwords.Count > Service.User.NumberOfOldPasswordToKeep + ? Passwords.Count - Service.User.NumberOfOldPasswordToKeep + : 0)]; + + foreach (DateTime dateToRemove in datesToRemove) + { + _ = Passwords.Remove(dateToRemove); + } + } + + _ = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(Password), + needsReview: true, + oldValue: oldPasswords, + newValue: Passwords, + readableValue: string.Empty); + } + } + } + } + + Dictionary IAccount.Passwords => new(Passwords); + + string IAccount.Notes + { + get => Database.Get(Notes); + set => Notes = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(Notes), + needsReview: false, + oldValue: Notes, + newValue: value, + readableValue: value); + } + + int IAccount.PasswordUpdateReminderDelay + { + get => Database.Get(PasswordUpdateReminderDelay); + set => PasswordUpdateReminderDelay = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(PasswordUpdateReminderDelay), + needsReview: false, + oldValue: PasswordUpdateReminderDelay, + newValue: value, + readableValue: value.ToString()); + } + + AccountOption IAccount.Options + { + get => Database.Get(Options); + set => Options = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(Options), + needsReview: false, + oldValue: Options, + newValue: value, + readableValue: value.ToString()); + } + + #endregion + + internal Database Database => Service.User.Database; + + public string ItemId { get; set; } = string.Empty; + + private Service? _service; + internal Service Service + { + get => _service ?? throw new NullReferenceException(nameof(Service)); + set => _service = value; + } + + public string Label { get; set; } = string.Empty; + public string[] Identifiers { get; set; } = []; + public string Password { get; set; } = string.Empty; + public Dictionary Passwords { get; set; } = []; + public string Notes { get; set; } = string.Empty; + public int PasswordUpdateReminderDelay { get; set; } = 0; + public AccountOption Options { get; set; } + = AccountOption.WarnIfPasswordLeaked; + + internal bool PasswordExpired + { + get + { + if (PasswordUpdateReminderDelay == 0) return false; + + DateTime lastPassword = Passwords.Keys.Max(); + int delay = ((DateTime.Now.Year - lastPassword.Year) * 12) + DateTime.Now.Month - lastPassword.Month; + + return delay > PasswordUpdateReminderDelay; + } + } + + internal bool PasswordLeaked { get; set; } = false; + + internal void Apply(Change change) + { + switch (change.ActionType) + { + case ActivityEventType.ItemUpdated: + switch (change.FieldName) + { + case nameof(Label): + Label = change.NewValue.DeserializeTo(Database.SerializationCenter); + break; + case nameof(Identifiers): + Identifiers = change.NewValue.DeserializeTo(Database.SerializationCenter); + break; + case nameof(Notes): + Notes = change.NewValue.DeserializeTo(Database.SerializationCenter); + break; + case nameof(Password): + Passwords = change.NewValue.DeserializeTo>(Database.SerializationCenter); + Password = Passwords.Count != 0 ? Passwords[Passwords.Keys.Max()] : string.Empty; + break; + case nameof(PasswordUpdateReminderDelay): + PasswordUpdateReminderDelay = change.NewValue.DeserializeTo(Database.SerializationCenter); + break; + case nameof(Options): + Options = change.NewValue.DeserializeTo(Database.SerializationCenter); + break; + default: + throw new InvalidDataException("FieldName not valid"); + } + break; + default: + throw new InvalidEnumArgumentException(nameof(change.ActionType), (int)change.ActionType, typeof(ActivityEventType)); + } + } + + public override string ToString() + { + string account = "Account "; + + if (!string.IsNullOrEmpty(Label)) + { + account += $"{Label} "; + } + + return account + $"({string.Join(", ", Identifiers)})"; + } + + public bool HasChanged() => Database.HasChanged(ItemId); + } +} \ No newline at end of file diff --git a/Core/Models/Activity.cs b/Core/Models/Activity.cs new file mode 100644 index 0000000..f033536 --- /dev/null +++ b/Core/Models/Activity.cs @@ -0,0 +1,114 @@ +using Upsilon.Apps.Passkey.Core.Utils; +using Upsilon.Apps.Passkey.Interfaces.Enums; +using Upsilon.Apps.Passkey.Interfaces.Models; + +namespace Upsilon.Apps.Passkey.Core.Models +{ + internal class Activity : IActivity + { + #region IActivity interface + + public DateTime DateTime => new(DateTimeTicks); + + public string ItemId { get; } = string.Empty; + + public ActivityEventType EventType { get; set; } = ActivityEventType.None; + + public bool NeedsReview { get; set; } = true; + + public string Message => _buildMessage(); + + #endregion + + public long DateTimeTicks { get; set; } + public string[] Data { get; set; } = []; + + public Activity(long dateTimeTicks, string itemId, ActivityEventType eventType, string[] data, bool needsReview) + { + DateTimeTicks = dateTimeTicks; + ItemId = itemId; + EventType = eventType; + Data = data; + NeedsReview = needsReview; + } + + public Activity(string activity) + { + string[] info = activity.Split('|'); + + if (info.Length > 0) + { + DateTimeTicks = Convert.ToInt64(info[0], 16); + } + + if (info.Length > 1) + { + ItemId = info[1]; + } + + if (info.Length > 2 + && byte.TryParse(info[2], out byte eventType)) + { + EventType = (ActivityEventType)eventType; + } + + if (info.Length > 3) + { + NeedsReview = !string.IsNullOrEmpty(info[3]); + } + + if (info.Length > 4) + { + activity = string.Join("|", info[4..]) + .Replace("|", "/|") + .Replace("\\/|", "\\|"); + info = activity.Split("/|"); + Data = [.. info.Select(x => x.Replace("\\|", "|"))]; + } + } + + public override string ToString() + { + string activity = $"{DateTimeTicks:X}|{ItemId}|{(int)EventType}|{(NeedsReview ? "1" : "")}"; + + string[] data = [.. Data.Select(x => x.Replace("|", "\\|"))]; + if (data.Length != 0) + { + activity += $"|{string.Join("|", data)}"; + } + + return activity; + } + + private string _buildMessage() + { + string message = EventType switch + { + ActivityEventType.MergeAndSaveThenRemoveAutoSaveFile => $"User {Data[0]}'s autosave merged and saved", + ActivityEventType.MergeWithoutSavingAndKeepAutoSaveFile => $"User {Data[0]}'s autosave merged without saving", + ActivityEventType.DontMergeAndRemoveAutoSaveFile => $"User {Data[0]}'s autosave not merged and removed", + ActivityEventType.DontMergeAndKeepAutoSaveFile => $"User {Data[0]}'s autosave not merged and keeped", + ActivityEventType.DatabaseCreated => $"User {Data[0]}'s database created", + ActivityEventType.DatabaseOpened => $"User {Data[0]}'s database opened", + ActivityEventType.DatabaseSaved => $"User {Data[0]}'s database saved", + ActivityEventType.DatabaseClosed => $"User {Data[0]}'s database closed", + ActivityEventType.LoginSessionTimeoutReached => $"User {Data[0]}'s login session timeout reached", + ActivityEventType.LoginFailed => $"User {Data[0]} login failed at level {Data[1]}", + ActivityEventType.UserLoggedIn => $"User {Data[0]} logged in", + ActivityEventType.UserLoggedOut => $"User {Data[0]} logged out {(!string.IsNullOrEmpty(Data[1]) ? "without saving" : "")}", + ActivityEventType.ImportingDataStarted => $"Importing data from file : '{Data[0]}'", + ActivityEventType.ImportingDataSucceded => $"Import completed successfully", + ActivityEventType.ImportingDataFailed => $"Import failed because {Data[0]}", + ActivityEventType.ExportingDataStarted => $"Exporting data to file : '{Data[0]}'", + ActivityEventType.ExportingDataSucceded => $"Export completed successfully", + ActivityEventType.ExportingDataFailed => $"Export failed because {Data[0]}", + ActivityEventType.ItemUpdated => $"{(Data.Length > 3 ? $"{Data[3]}'s " : "")}{Data[0]}'s {Data[1].ToSentenceCase().ToLower()} has been {(string.IsNullOrWhiteSpace(Data[2]) ? $"updated" : $"set to {Data[2]}")}", + ActivityEventType.ItemAdded => $"{Data[2]} has been added to {Data[0]}", + ActivityEventType.ItemDeleted => $"{Data[2]} has been removed from {Data[0]}", + _ => ToString(), + }; + + return message.Trim(); + } + } +} diff --git a/Core/Models/AutoSave.cs b/Core/Models/AutoSave.cs new file mode 100644 index 0000000..9516c9d --- /dev/null +++ b/Core/Models/AutoSave.cs @@ -0,0 +1,203 @@ +using Upsilon.Apps.Passkey.Core.Utils; +using Upsilon.Apps.Passkey.Interfaces.Enums; + +namespace Upsilon.Apps.Passkey.Core.Models +{ + internal sealed class AutoSave + { + internal Database Database + { + get => field ?? throw new NullReferenceException(nameof(Database)); + set; + } + + public Dictionary> Changes { get; set; } = []; + + internal T UpdateValue(string itemId, + string fieldName, + bool needsReview, + T oldValue, + T newValue, + string readableValue) where T : notnull + { + if (Database.SerializationCenter.AreDifferent(oldValue, newValue)) + { + _addChange(itemId, + fieldName, + oldValue.SerializeWith(Database.SerializationCenter), + newValue.SerializeWith(Database.SerializationCenter), + readableValue, + needsReview, + ActivityEventType.ItemUpdated); + } + + return newValue; + } + + internal T AddValue(string itemId, + string readableValue, + bool needsReview, + T value) where T : notnull + { + _addChange(itemId, string.Empty, value.SerializeWith(Database.SerializationCenter), readableValue, needsReview, ActivityEventType.ItemAdded); + + return value; + } + + internal T DeleteValue(string itemId, + string readableValue, + bool needsReview, + T value) where T : notnull + { + _addChange(itemId, string.Empty, value.SerializeWith(Database.SerializationCenter), readableValue, needsReview, ActivityEventType.ItemDeleted); + + return value; + } + + private void _addChange(string itemId, + string fieldName, + string newValue, + string readableValue, + bool needsReview, + ActivityEventType action) + { + _addChange(itemId, + fieldName, + null, + newValue, + readableValue, + needsReview, + action); + } + + private void _addChange(string itemId, + string fieldName, + string? oldValue, + string newValue, + string readableValue, + bool needsReview, + ActivityEventType action) + { + string changeKey = $"{itemId}\t{fieldName}"; + if (!Changes.ContainsKey(changeKey)) + { + Changes[changeKey] = []; + } + + Change currentChange = new() + { + Index = DateTime.Now.Ticks, + ActionType = action, + ItemId = itemId, + FieldName = fieldName, + OldValue = oldValue, + NewValue = newValue, + }; + + _mergeChanges(changeKey, currentChange); + + Database.FileLocker.Save(this, Database.AutoSaveFileEntry, Database.Passkeys); + string itemName = string.Empty; + string parentName = string.Empty; + + if (itemId == Database.User?.ItemId) + { + if (Database.User is not null) + { + itemName = Database.User.ToString(); + } + } + else if (itemId.StartsWith('S')) + { + Service? s = Database.User?.Services.FirstOrDefault(x => x.ItemId == itemId); + + if (s is not null) + { + itemName = s.ToString(); + } + } + else if (itemId.StartsWith('A')) + { + Account? a = Database.User?.Services.SelectMany(x => x.Accounts).FirstOrDefault(x => x.ItemId == itemId); + + if (a is not null) + { + itemName = a.ToString(); + + if (action == ActivityEventType.ItemUpdated) + { + parentName = a.Service.ToString(); + } + } + } + + string[] data = [itemName, fieldName, readableValue]; + + if (!string.IsNullOrEmpty(parentName)) + { + data = [.. data, parentName]; + } + + Database.ActivityCenter.AddActivity(itemId: itemId, + eventType: action, + data, + needsReview); + } + + private void _mergeChanges(string changeKey, Change currentChange) + { + Change? lastUpdate = Changes[changeKey].LastOrDefault(x => x.ActionType == ActivityEventType.ItemUpdated); + + if (currentChange.ActionType != ActivityEventType.ItemUpdated + || lastUpdate is null) + { + Changes[changeKey].Add(currentChange); + return; + } + + _ = Changes[changeKey].Remove(lastUpdate); + currentChange.OldValue = lastUpdate.OldValue; + + if (currentChange.OldValue != currentChange.NewValue) + { + Changes[changeKey].Add(currentChange); + } + else if (Changes[changeKey].Count == 0) + { + _ = Changes.Remove(changeKey); + } + } + + internal void ApplyChanges(bool deleteFile) + { + List changes = [.. Changes.Values.SelectMany(x => x).OrderBy(x => x.Index)]; + + foreach (Change change in changes) + { + Database.User?.Apply(change); + } + + if (deleteFile) + { + Clear(deleteFile: true); + } + } + + internal bool Any() => Any(string.Empty); + + internal bool Any(string itemId) => Changes.Any(x => x.Key.StartsWith(itemId)); + + internal bool Any(string itemId, string fieldName) => Changes.Any(x => x.Key == $"{itemId}\t{fieldName}"); + + internal void Clear(bool deleteFile) + { + Changes.Clear(); + + if (deleteFile + && Database.FileLocker.Exists(Database.AutoSaveFileEntry)) + { + Database.FileLocker.Delete(Database.AutoSaveFileEntry); + } + } + } +} \ No newline at end of file diff --git a/Core/Models/Change.cs b/Core/Models/Change.cs new file mode 100644 index 0000000..ca78fb7 --- /dev/null +++ b/Core/Models/Change.cs @@ -0,0 +1,14 @@ +using Upsilon.Apps.Passkey.Interfaces.Enums; + +namespace Upsilon.Apps.Passkey.Core.Models +{ + internal sealed class Change + { + public long Index { get; set; } = long.MaxValue; + public ActivityEventType ActionType { get; set; } = ActivityEventType.None; + public string ItemId { get; set; } = string.Empty; + public string FieldName { get; set; } = string.Empty; + public string? OldValue { get; set; } = null; + public string NewValue { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Core/Models/Database.cs b/Core/Models/Database.cs new file mode 100644 index 0000000..5af692b --- /dev/null +++ b/Core/Models/Database.cs @@ -0,0 +1,541 @@ +using Upsilon.Apps.Passkey.Core.Utils; +using Upsilon.Apps.Passkey.Interfaces.Enums; +using Upsilon.Apps.Passkey.Interfaces.Events; +using Upsilon.Apps.Passkey.Interfaces.Models; +using Upsilon.Apps.Passkey.Interfaces.Utils; + +namespace Upsilon.Apps.Passkey.Core.Models +{ + public sealed class Database : IDatabase + { + #region IUser interface explicit Internal + + public string DatabaseFile { get; set; } + + IUser? IDatabase.User => User; + int? IDatabase.SessionLeftTime => User?.SessionLeftTime; + + IActivity[]? IDatabase.Activities => Get(ActivityCenter.Activities.OrderByDescending(x => x.DateTime).ToArray()); + + IWarning[]? IDatabase.Warnings => Get(User is not null ? Warnings : null); + + public ICryptographyCenter CryptographyCenter { get; private set; } + public ISerializationCenter SerializationCenter { get; private set; } + public IPasswordFactory PasswordFactory { get; private set; } + public IClipboardManager ClipboardManager { get; private set; } + + public event EventHandler? WarningsUpdated; + public event EventHandler? AutoSaveDetected; + public event EventHandler? DatabaseSaved; + public event EventHandler? DatabaseClosed; + + public void Delete() + { + if (User is null) throw new NullReferenceException(nameof(User)); + + FileLocker.Delete(); + + Close(logCloseEvent: false, loginTimeoutReached: false); + } + + public void Dispose() => Close(logCloseEvent: true, loginTimeoutReached: false); + + public void Save() => _save(logSaveEvent: true); + + public IUser? Login(string passkey) + { + Passkeys = [.. Passkeys, CryptographyCenter.GetSlowHash(passkey)]; + + try + { + User = FileLocker.Open(DatabaseFileEntry, Passkeys); + } + catch (Exception ex) + { + if (ex is WrongPasswordException passwordException) + { + ActivityCenter.AddActivity(itemId: string.Empty, + eventType: ActivityEventType.LoginFailed, + data: [Username, passwordException.PasswordLevel.ToString()], + needsReview: true); + } + } + + if (User is not null) + { + User.Database = this; + + ActivityCenter.LoadStringActivities(); + ActivityCenter.AddActivity(itemId: string.Empty, + eventType: ActivityEventType.UserLoggedIn, + data: [Username], + needsReview: false); + + if (FileLocker.Exists(AutoSaveFileEntry)) + { + AutoSave = FileLocker.Open(AutoSaveFileEntry, Passkeys); + AutoSave.Database = this; + + AutoSaveDetectedEventArgs eventArg = new(); + AutoSaveDetected?.Invoke(this, eventArg); + _handleAutoSave(eventArg.MergeBehavior); + } + + _ = Task.Run(_lookAtWarnings); + + User.ResetTimer(); + } + + return User; + } + + public void Close() => Dispose(); + + public bool HasChanged(string itemId) => AutoSave.Any(itemId); + + public bool HasChanged(string itemId, string fieldName) => AutoSave.Any(itemId, fieldName); + + public bool ImportFromFile(string filePath) + { + if (User is null) throw new NullReferenceException(nameof(User)); + + if (User.HasChanged()) + { + _save(logSaveEvent: true); + } + + ActivityCenter.AddActivity(itemId: string.Empty, + eventType: ActivityEventType.ImportingDataStarted, + data: [filePath], + needsReview: true); + + string importContent = string.Empty; + string errorLog = string.Empty; + + try + { + importContent = File.ReadAllText(filePath); + } + catch + { + errorLog = $"import file is not accessible"; + } + + if (string.IsNullOrWhiteSpace(errorLog)) + { + string extention = Path.GetExtension(filePath); + + errorLog = extention switch + { + ".json" => this.ImportJson(importContent), + ".csv" => this.ImportCSV(importContent), + _ => $"'{extention}' extention type is not handled", + }; + } + + if (string.IsNullOrWhiteSpace(errorLog)) + { + ActivityCenter.AddActivity(itemId: string.Empty, + eventType: ActivityEventType.ImportingDataSucceded, + data: [], + needsReview: true); + _save(logSaveEvent: true); + } + else + { + ActivityCenter.AddActivity(itemId: string.Empty, + eventType: ActivityEventType.ImportingDataFailed, + data: [errorLog], + needsReview: true); + } + + return string.IsNullOrWhiteSpace(errorLog); + } + + public bool ExportToFile(string filePath) + { + if (User is null) throw new NullReferenceException(nameof(User)); + + if (User.HasChanged()) + { + _save(logSaveEvent: true); + } + + ActivityCenter.AddActivity(itemId: string.Empty, + eventType: ActivityEventType.ExportingDataStarted, + data: [filePath], + needsReview: true); + + string errorLog = string.Empty; + + if (File.Exists(filePath)) + { + errorLog = $"export file already exists"; + } + + if (string.IsNullOrWhiteSpace(errorLog)) + { + string extention = Path.GetExtension(filePath); + + errorLog = extention switch + { + ".json" => this.ExportJson(filePath), + ".csv" => this.ExportCSV(filePath), + _ => $"'{extention}' extention type is not handled", + }; + } + + if (string.IsNullOrWhiteSpace(errorLog)) + { + ActivityCenter.AddActivity(itemId: string.Empty, + eventType: ActivityEventType.ExportingDataSucceded, + data: [], + needsReview: true); + } + else + { + ActivityCenter.AddActivity(itemId: string.Empty, + eventType: ActivityEventType.ExportingDataFailed, + data: [errorLog], + needsReview: true); + } + + return string.IsNullOrWhiteSpace(errorLog); + } + + #endregion + + internal User? User; + internal AutoSave AutoSave; + internal ActivityCenter ActivityCenter; + internal Warning[]? Warnings; + + internal string Username { get; private set; } + internal string[] Passkeys { get; private set; } + + internal readonly string DatabaseFileEntry = "database"; + internal readonly string AutoSaveFileEntry = "autosave"; + internal readonly string ActivityFileEntry = "activity"; + internal FileLocker FileLocker; + + private Database(ICryptographyCenter cryptographicCenter, + ISerializationCenter serializationCenter, + IPasswordFactory passwordFactory, + IClipboardManager clipboardManager, + string databaseFile, + FileMode fileMode, + string username, + string publicKey = "", + string[]? passkeys = null) + { + DatabaseFile = databaseFile; + + CryptographyCenter = cryptographicCenter; + SerializationCenter = serializationCenter; + PasswordFactory = passwordFactory; + ClipboardManager = clipboardManager; + + Username = username; + Passkeys = [CryptographyCenter.GetHash(username)]; + + if (passkeys is not null) + { + Passkeys = [.. Passkeys, .. passkeys.Select(x => CryptographyCenter.GetSlowHash(x))]; + } + + AutoSave = new() + { + Database = this, + }; + + FileLocker = new(cryptographicCenter, serializationCenter, databaseFile, fileMode); + + ActivityCenter = fileMode == FileMode.Create + ? new() + { + Username = username, + PublicKey = publicKey, + } + : FileLocker.Open(ActivityFileEntry); + + ActivityCenter.Database = this; + } + + public static IDatabase Create(ICryptographyCenter cryptographicCenter, + ISerializationCenter serializationCenter, + IPasswordFactory passwordFactory, + IClipboardManager clipboardManager, + string databaseFile, + string username, + string[] passkeys) + { + if (File.Exists(databaseFile)) + { + throw new IOException($"'{databaseFile}' database file already exists"); + } + + string databaseFileDirectory = Path.GetDirectoryName(databaseFile) ?? string.Empty; + + if (!Directory.Exists(databaseFileDirectory)) + { + _ = Directory.CreateDirectory(databaseFileDirectory); + } + + cryptographicCenter.GenerateRandomKeys(out string publicKey, out string privateKey); + + Database database = new(cryptographicCenter, + serializationCenter, + passwordFactory, + clipboardManager, + databaseFile, + FileMode.Create, + username, + publicKey, + passkeys); + + database.User = new() + { + Database = database, + PrivateKey = privateKey, + ItemId = "U" + cryptographicCenter.GetHash(username), + Username = username, + Passkeys = [.. passkeys], + }; + + database.ActivityCenter.AddActivity(itemId: string.Empty, + eventType: ActivityEventType.DatabaseCreated, + data: [username], + needsReview: false); + + database._save(logSaveEvent: false); + + return database; + } + + public static IDatabase Open(ICryptographyCenter cryptographicCenter, + ISerializationCenter serializationCenter, + IPasswordFactory passwordFactory, + IClipboardManager clipboardManager, + string databaseFile, + string username) + { + Database database = new(cryptographicCenter, + serializationCenter, + passwordFactory, + clipboardManager, + databaseFile, + FileMode.Open, + username); + + database.ActivityCenter.AddActivity(itemId: string.Empty, + eventType: ActivityEventType.DatabaseOpened, + data: [username], + needsReview: false); + + return database; + } + + internal T Get(T value) + { + User?.ResetTimer(); + + return value; + } + + private void _save(bool logSaveEvent) + { + _saveActivities(rebuildStringActivities: true); + _saveDatabase(logSaveEvent); + } + + private void _saveDatabase(bool logSaveEvent) + { + if (User is null) throw new NullReferenceException(nameof(User)); + + Username = User.Username; + Passkeys = [CryptographyCenter.GetHash(User.Username), .. User.Passkeys.Select(CryptographyCenter.GetSlowHash)]; + FileLocker.Save(User, DatabaseFileEntry, Passkeys); + + if (logSaveEvent) + { + ActivityCenter.AddActivity(itemId: string.Empty, + eventType: ActivityEventType.DatabaseSaved, + data: [Username], + needsReview: false); + } + + AutoSave.Clear(deleteFile: true); + + _ = Task.Run(_lookAtWarnings); + + User.ResetTimer(); + + DatabaseSaved?.Invoke(this, EventArgs.Empty); + } + + private void _saveActivities(bool rebuildStringActivities) + { + if (User is null) throw new NullReferenceException(nameof(User)); + + ActivityCenter.Username = User.Username; + ActivityCenter.Save(rebuildStringActivities); + } + + internal void Close(bool logCloseEvent, bool loginTimeoutReached) + { + if (logCloseEvent) + { + if (User is not null) + { + bool needsReview = AutoSave.Any(); + + if (!needsReview) + { + AutoSave.Clear(deleteFile: true); + } + + ActivityCenter.AddActivity(itemId: string.Empty, + eventType: ActivityEventType.UserLoggedOut, + data: [Username, needsReview ? "1" : string.Empty], + needsReview); + } + + ActivityCenter.AddActivity(itemId: string.Empty, + eventType: ActivityEventType.DatabaseClosed, + data: [Username], + needsReview: false); + } + + User = null; + Username = string.Empty; + Passkeys = []; + Warnings = null; + + FileLocker.Dispose(); + + DatabaseClosed?.Invoke(this, new(loginTimeoutReached)); + } + + private void _handleAutoSave(AutoSaveMergeBehavior mergeAutoSave) + { + if (User is null) throw new NullReferenceException(nameof(User)); + + if (!FileLocker.Exists(AutoSaveFileEntry)) + { + return; + } + + switch (mergeAutoSave) + { + case AutoSaveMergeBehavior.MergeAndSaveThenRemoveAutoSaveFile: + AutoSave.ApplyChanges(deleteFile: true); + _save(logSaveEvent: false); + break; + case AutoSaveMergeBehavior.MergeWithoutSavingAndKeepAutoSaveFile: + AutoSave.ApplyChanges(deleteFile: false); + _saveActivities(rebuildStringActivities: false); + break; + case AutoSaveMergeBehavior.DontMergeAndRemoveAutoSaveFile: + AutoSave.Clear(deleteFile: true); + break; + case AutoSaveMergeBehavior.DontMergeAndKeepAutoSaveFile: + default: + break; + } + + ActivityCenter.AddActivity(itemId: string.Empty, + eventType: (ActivityEventType)mergeAutoSave, + data: [Username], + needsReview: true); + } + + private void _lookAtWarnings() + { + if (User is null) return; + + try + { + Warning[] activityWarnings = _lookAtActivityWarnings(); + Warning[] passwordUpdateReminderWarnings = _lookAtPasswordUpdateReminderWarnings(); + Warning[] passwordLeakedWarnings = _lookAtPasswordLeakedWarnings(); + Warning[] duplicatedPasswordsWarnings = _lookAtDuplicatedPasswordsWarnings(); + + Warnings = [..activityWarnings, + ..passwordUpdateReminderWarnings, + ..passwordLeakedWarnings, + ..duplicatedPasswordsWarnings]; + + WarningsUpdated?.Invoke(this, new WarningsUpdatedEventArgs([.. Warnings.Where(x => User.WarningsToNotify.HasFlag(x.WarningType))])); + } + catch { } + } + + private Warning[] _lookAtActivityWarnings() + { + if (User is null) throw new NullReferenceException(nameof(User)); + if (ActivityCenter.Activities is null) throw new NullReferenceException(nameof(ActivityCenter.Activities)); + + IActivity[] activities = [.. ActivityCenter.Activities.Where(x => x.NeedsReview)]; + + return activities.Length != 0 ? [new Warning([.. activities])] : []; + } + + private Warning[] _lookAtPasswordUpdateReminderWarnings() + { + if (User is null) return []; + + Account[] accounts = [.. User.Services + .SelectMany(x => x.Accounts) + .Where(x => x.PasswordExpired)]; + + return accounts.Length != 0 ? [new Warning(WarningType.PasswordUpdateReminderWarning, accounts)] : []; + } + + private Warning[] _lookAtPasswordLeakedWarnings() + { + if (User is null) return []; + + string[] leakedPasswords = [.. User.Services + .SelectMany(x => x.Accounts) + .Where(x => x.Options.HasFlag(AccountOption.WarnIfPasswordLeaked)) + .Select(x => x.Password) + .Distinct() + .AsParallel() + .Where(PasswordFactory.PasswordLeaked)]; + + Account[] accounts = [.. User.Services + .SelectMany(x => x.Accounts) + .Where(x => x.Options.HasFlag(AccountOption.WarnIfPasswordLeaked) + && leakedPasswords.Contains(x.Password))]; + + foreach (Account account in accounts) + { + account.PasswordLeaked = true; + } + + return accounts.Length != 0 ? [new Warning(WarningType.PasswordLeakedWarning, accounts)] : []; + } + + private Warning[] _lookAtDuplicatedPasswordsWarnings() + { + if (User is null) return []; + + IGrouping[] duplicatedPasswords = [.. User.Services + .SelectMany(x => x.Accounts) + .GroupBy(x => x.Password) + .Where(x => x.Count() > 1)]; + + List warnings = []; + + foreach (IGrouping accounts in duplicatedPasswords) + { + if (accounts.Any(x => x.Options.HasFlag(AccountOption.WarnIfDuplicatedPassword))) + { + warnings.Add(new(WarningType.DuplicatedPasswordsWarning, [.. accounts.Cast()])); + } + } + + return [.. warnings]; + } + } +} \ No newline at end of file diff --git a/Core/Models/Service.cs b/Core/Models/Service.cs new file mode 100644 index 0000000..99062a8 --- /dev/null +++ b/Core/Models/Service.cs @@ -0,0 +1,162 @@ +using System.ComponentModel; +using Upsilon.Apps.Passkey.Core.Utils; +using Upsilon.Apps.Passkey.Interfaces.Enums; +using Upsilon.Apps.Passkey.Interfaces.Models; + +namespace Upsilon.Apps.Passkey.Core.Models +{ + internal sealed class Service : IService + { + #region IService interface explicit Internal + + string IItem.ItemId => Database.Get(ItemId); + + IDatabase IItem.Database => Database; + + IUser IService.User => Database.Get(User); + IAccount[] IService.Accounts => [.. Database.Get(Accounts)]; + + string IService.ServiceName + { + get => Database.Get(ServiceName); + set => ServiceName = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(ServiceName), + needsReview: true, + oldValue: ServiceName, + newValue: value, + readableValue: value); + } + + string IService.Url + { + get => Database.Get(Url); + set => Url = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(Url), + needsReview: false, + oldValue: Url, + newValue: value, + readableValue: value); + } + + string IService.Notes + { + get => Database.Get(Notes); + set => Notes = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(Notes), + needsReview: false, + oldValue: Notes, + newValue: value, + readableValue: value); + } + + public IAccount AddAccount(string label, IEnumerable identifiers, string password) + { + Account account = new() + { + Service = this, + ItemId = "A" + Database.CryptographyCenter.GetHash(ItemId + label + string.Join(string.Empty, identifiers)), + Label = label, + Identifiers = [.. identifiers], + Password = password, + }; + + Accounts.Add(Database.AutoSave.AddValue(ItemId, readableValue: account.ToString(), needsReview: false, account)); + + account.Passwords[DateTime.Now] = Database.AutoSave.UpdateValue(account.ItemId, + fieldName: nameof(account.Password), + needsReview: true, + oldValue: string.Empty, + newValue: account.Password, + readableValue: string.Empty); + + return account; + } + + public IAccount AddAccount(string label, IEnumerable identifiers) + { + return AddAccount(label, identifiers, password: string.Empty); + } + + public IAccount AddAccount(IEnumerable identifiers, string password) + { + return AddAccount(label: string.Empty, identifiers, password); + } + + public IAccount AddAccount(IEnumerable identifiers) + { + return AddAccount(label: string.Empty, identifiers, password: string.Empty); + } + + void IService.DeleteAccount(IAccount account) + { + Account accountToRemove = Accounts.FirstOrDefault(x => x.ItemId == account.ItemId) + ?? throw new KeyNotFoundException($"The {account}' was not found into the {this}'s accounts list"); + + _ = Accounts.Remove(Database.AutoSave.DeleteValue(ItemId, readableValue: accountToRemove.ToString(), needsReview: true, accountToRemove)); + } + + #endregion + + internal Database Database => User.Database; + + public string ItemId { get; set; } = string.Empty; + + internal User User + { + get => field ?? throw new NullReferenceException(nameof(User)); + set + { + field = value; + + foreach (Account account in Accounts) + { + account.Service = this; + } + } + } + + public List Accounts { get; set; } = []; + + public string ServiceName { get; set; } = string.Empty; + public string Url { get; set; } = string.Empty; + public string Notes { get; set; } = string.Empty; + + internal void Apply(Change change) + { + switch (change.ActionType) + { + case ActivityEventType.ItemUpdated: + switch (change.FieldName) + { + case nameof(ServiceName): + ServiceName = change.NewValue.DeserializeTo(Database.SerializationCenter); + break; + case nameof(Url): + Url = change.NewValue.DeserializeTo(Database.SerializationCenter); + break; + case nameof(Notes): + Notes = change.NewValue.DeserializeTo(Database.SerializationCenter); + break; + default: + throw new InvalidDataException("FieldName not valid"); + } + break; + case ActivityEventType.ItemAdded: + Account accountToAdd = change.NewValue.DeserializeTo(Database.SerializationCenter); + accountToAdd.Service = this; + Accounts.Add(accountToAdd); + break; + case ActivityEventType.ItemDeleted: + Account accountToDelete = change.NewValue.DeserializeTo(Database.SerializationCenter); + _ = Accounts.RemoveAll(x => x.ItemId == accountToDelete.ItemId); + break; + default: + throw new InvalidEnumArgumentException(nameof(change.ActionType), (int)change.ActionType, typeof(ActivityEventType)); + } + } + + public override string ToString() => $"Service {ServiceName}"; + + public bool HasChanged() => Database.HasChanged(ItemId) || Accounts.Any(x => x.HasChanged()); + } +} \ No newline at end of file diff --git a/Core/Models/User.cs b/Core/Models/User.cs new file mode 100644 index 0000000..bb22aed --- /dev/null +++ b/Core/Models/User.cs @@ -0,0 +1,319 @@ +using System.ComponentModel; +using Upsilon.Apps.Passkey.Core.Utils; +using Upsilon.Apps.Passkey.Interfaces.Enums; +using Upsilon.Apps.Passkey.Interfaces.Models; + +namespace Upsilon.Apps.Passkey.Core.Models +{ + internal sealed class User : IUser + { + #region IUser interface explicit Internal + + string IItem.ItemId => Database.Get(ItemId); + + IDatabase IItem.Database => Database; + + IService[] IUser.Services => [.. Database.Get(Services)]; + + string IUser.Username + { + get => Database.Get(Username); + set + { + CredentialChanged |= Username != value; + + Username = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(Username), + needsReview: true, + oldValue: Username, + newValue: value, + readableValue: value); + } + } + + string[] IUser.Passkeys + { + get => Database.Get(Passkeys); + set + { + CredentialChanged |= Database.SerializationCenter.AreDifferent(Passkeys, value); + + Passkeys = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(Passkeys), + needsReview: true, + oldValue: Passkeys, + newValue: value, + readableValue: string.Empty); + } + } + + int IUser.LogoutTimeout + { + get => Database.Get(LogoutTimeout); + set => LogoutTimeout = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(LogoutTimeout), + needsReview: false, + oldValue: LogoutTimeout, + newValue: value, + readableValue: value.ToString()); + } + + int IUser.CleaningClipboardTimeout + { + get => Database.Get(CleaningClipboardTimeout); + set => CleaningClipboardTimeout = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(CleaningClipboardTimeout), + needsReview: false, + oldValue: CleaningClipboardTimeout, + newValue: value, + readableValue: value.ToString()); + } + + int IUser.ShowPasswordDelay + { + get => Database.Get(ShowPasswordDelay); + set => ShowPasswordDelay = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(ShowPasswordDelay), + needsReview: false, + oldValue: ShowPasswordDelay, + newValue: value, + readableValue: value.ToString()); + } + + int IUser.NumberOfOldPasswordToKeep + { + get => Database.Get(NumberOfOldPasswordToKeep); + set + { + NumberOfOldPasswordToKeep = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(NumberOfOldPasswordToKeep), + needsReview: true, + oldValue: NumberOfOldPasswordToKeep, + newValue: value, + readableValue: value.ToString()); + + if (NumberOfOldPasswordToKeep == 0) return; + + Account[] accounts = [.. Services.SelectMany(x => x.Accounts).Where(x => x.Passwords.Count > NumberOfOldPasswordToKeep)]; + + foreach (Account account in accounts) + { + DateTime[] datesToRemove = [.. account.Passwords.Keys + .OrderBy(x => x) + .Take(account.Passwords.Count - NumberOfOldPasswordToKeep)]; + + foreach (DateTime dateToRemove in datesToRemove) + { + _ = account.Passwords.Remove(dateToRemove); + } + } + } + } + + int IUser.NumberOfMonthActivitiesToKeep + { + get => Database.Get(NumberOfMonthActivitiesToKeep); + set + { + NumberOfMonthActivitiesToKeep = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(NumberOfMonthActivitiesToKeep), + needsReview: true, + oldValue: NumberOfMonthActivitiesToKeep, + newValue: value, + readableValue: value.ToString()); + + Database.ActivityCenter.Save(rebuildStringActivities: true); + } + } + + WarningType IUser.WarningsToNotify + { + get => Database.Get(WarningsToNotify); + set => WarningsToNotify = Database.AutoSave.UpdateValue(ItemId, + fieldName: nameof(WarningsToNotify), + needsReview: true, + oldValue: WarningsToNotify, + newValue: value, + readableValue: value.ToString()); + } + + IService IUser.AddService(string serviceName) + { + Service service = new() + { + User = this, + ItemId = "S" + Database.CryptographyCenter.GetHash(ItemId + serviceName), + ServiceName = serviceName + }; + + Services.Add(Database.AutoSave.AddValue(ItemId, readableValue: service.ToString(), needsReview: false, value: service)); + + return service; + } + + void IUser.DeleteService(IService service) + { + Service serviceToRemove = Services.FirstOrDefault(x => x.ItemId == service.ItemId) + ?? throw new KeyNotFoundException($"The {service} was not found into the {this}'s services list"); + + _ = Services.Remove(Database.AutoSave.DeleteValue(ItemId, readableValue: serviceToRemove.ToString(), needsReview: true, value: serviceToRemove)); + } + + #endregion + + internal Database Database + { + get => field ?? throw new NullReferenceException(nameof(Database)); + set + { + field = value; + + foreach (Service service in Services) + { + service.User = this; + } + } + } + + public string PrivateKey { get; set; } = string.Empty; + + public string ItemId { get; set; } = string.Empty; + public List Services { get; set; } = []; + + public string Username { get; set; } = string.Empty; + public string[] Passkeys { get; set; } = []; + public bool CredentialChanged { get; set; } = false; + public int LogoutTimeout { get; set; } = 0; + public int CleaningClipboardTimeout { get; set; } = 0; + public int ShowPasswordDelay { get; set; } = 0; + public int NumberOfOldPasswordToKeep { get; set; } = 0; + public int NumberOfMonthActivitiesToKeep { get; set; } = 0; + public WarningType WarningsToNotify { get; set; } + = WarningType.ActivityReviewWarning + | WarningType.PasswordUpdateReminderWarning + | WarningType.DuplicatedPasswordsWarning + | WarningType.PasswordLeakedWarning; + + private readonly System.Timers.Timer _timer = new() + { + AutoReset = true, + Enabled = true, + Interval = 1000, + }; + + public int SessionLeftTime = 0; + private int _clipboardLeftTime = 0; + + public User() + { + _timer.Elapsed += _timer_Elapsed; + } + + private void _timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) + { + if (LogoutTimeout != 0) + { + SessionLeftTime--; + + if (SessionLeftTime == 0) + { + Database.ActivityCenter.AddActivity(itemId: ItemId, + eventType: ActivityEventType.LoginSessionTimeoutReached, + data: [Username], + needsReview: true); + Database.Close(logCloseEvent: true, loginTimeoutReached: true); + + _timer.Stop(); + _timer.Dispose(); + } + } + + if (CleaningClipboardTimeout != 0) + { + _clipboardLeftTime--; + + if (_clipboardLeftTime == 0) + { + _ = Database.ClipboardManager.RemoveAllOccurence([.. Services.SelectMany(x => x.Accounts).SelectMany(x => x.Passwords.Values)]); + _clipboardLeftTime = CleaningClipboardTimeout; + } + } + } + + public void ResetTimer() + { + SessionLeftTime = LogoutTimeout * 60; + _clipboardLeftTime = CleaningClipboardTimeout; + } + + internal void Apply(Change change) + { + switch (change.ItemId[0]) + { + case 'U': + _apply(change); + break; + case 'S': + Service service = Services.FirstOrDefault(x => change.ItemId == x.ItemId) + ?? throw new KeyNotFoundException($"The Service '{change.ItemId}' was not found into the {this}'s services list"); + + service.Apply(change); + break; + case 'A': + Account account = Services.SelectMany(x => x.Accounts).FirstOrDefault(x => change.ItemId == x.ItemId) + ?? throw new KeyNotFoundException($"The Account {change.ItemId}' was not found into the {this}'s accounts list"); + + account.Apply(change); + break; + default: + throw new InvalidDataException("ItemId not valid"); + } + } + + private void _apply(Change change) + { + switch (change.ActionType) + { + case ActivityEventType.ItemUpdated: + switch (change.FieldName) + { + case nameof(Username): + CredentialChanged = true; + Username = change.NewValue.DeserializeTo(Database.SerializationCenter); + break; + case nameof(Passkeys): + CredentialChanged = true; + Passkeys = change.NewValue.DeserializeTo(Database.SerializationCenter); + break; + case nameof(LogoutTimeout): + LogoutTimeout = change.NewValue.DeserializeTo(Database.SerializationCenter); + break; + case nameof(CleaningClipboardTimeout): + CleaningClipboardTimeout = change.NewValue.DeserializeTo(Database.SerializationCenter); + break; + case nameof(WarningsToNotify): + WarningsToNotify = change.NewValue.DeserializeTo(Database.SerializationCenter); + break; + default: + throw new InvalidDataException("FieldName not valid"); + } + break; + case ActivityEventType.ItemAdded: + Service serviceToAdd = change.NewValue.DeserializeTo(Database.SerializationCenter); + serviceToAdd.User = this; + Services.Add(serviceToAdd); + break; + case ActivityEventType.ItemDeleted: + Service serviceToDelete = change.NewValue.DeserializeTo(Database.SerializationCenter); + _ = Services.RemoveAll(x => x.ItemId == serviceToDelete.ItemId); + break; + default: + throw new InvalidEnumArgumentException(nameof(change.ActionType), (int)change.ActionType, typeof(ActivityEventType)); + } + } + + public override string ToString() => $"User {Database.Username}"; + + public bool HasChanged() => Database.HasChanged(ItemId) || Services.Any(x => x.HasChanged()); + } +} \ No newline at end of file diff --git a/Core/Models/Warning.cs b/Core/Models/Warning.cs new file mode 100644 index 0000000..a9f257e --- /dev/null +++ b/Core/Models/Warning.cs @@ -0,0 +1,26 @@ +using Upsilon.Apps.Passkey.Interfaces.Enums; +using Upsilon.Apps.Passkey.Interfaces.Models; + +namespace Upsilon.Apps.Passkey.Core.Models +{ + internal class Warning : IWarning + { + public WarningType WarningType { get; set; } + + public IActivity[]? Activities { get; set; } + + public IAccount[]? Accounts { get; set; } + + public Warning(IActivity[] activities) + { + WarningType = WarningType.ActivityReviewWarning; + Activities = activities; + } + + public Warning(WarningType warningType, IAccount[] accounts) + { + WarningType = warningType; + Accounts = accounts; + } + } +} diff --git a/Core/Upsilon.Apps.Passkey.Core.csproj b/Core/Upsilon.Apps.Passkey.Core.csproj new file mode 100644 index 0000000..7ef6b33 --- /dev/null +++ b/Core/Upsilon.Apps.Passkey.Core.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + $(AssemblyName) + Yassin Lokhat + A local stored Password Manager Core. + 1.0.1 + 1.0.1 + + + + True + 9999 + + + + True + 9999 + + + + + + + diff --git a/Core/Utils/ActivityCenter.cs b/Core/Utils/ActivityCenter.cs new file mode 100644 index 0000000..c3ae69a --- /dev/null +++ b/Core/Utils/ActivityCenter.cs @@ -0,0 +1,73 @@ +using Upsilon.Apps.Passkey.Core.Models; +using Upsilon.Apps.Passkey.Interfaces.Enums; +using Upsilon.Apps.Passkey.Interfaces.Models; + +namespace Upsilon.Apps.Passkey.Core.Utils +{ + internal class ActivityCenter + { + internal Database Database + { + get => field ?? throw new NullReferenceException(nameof(Database)); + set; + } + + internal List Activities = []; + + public List ActivityList { get; set; } = []; + + public string Username { get; set; } = string.Empty; + + public string PublicKey { get; set; } = string.Empty; + + internal void AddActivity(string itemId, ActivityEventType eventType, string[] data, bool needsReview) + { + Activity activity = new(DateTime.Now.Ticks, itemId, eventType, data, needsReview); + + Activities.Insert(0, activity); + ActivityList.Insert(0, Database.CryptographyCenter.EncryptAsymmetrically(activity.ToString(), PublicKey)); + + Save(rebuildStringActivities: false); + } + + internal void LoadStringActivities() + { + Activities.Clear(); + + if (Database.User is null) return; + + Activities = [.. ActivityList.AsParallel() + .Select(x => new Activity(Database.CryptographyCenter.DecryptAsymmetrically(x, Database.User.PrivateKey))) + .OrderByDescending(x => x.DateTime)]; + } + + internal void Save(bool rebuildStringActivities) + { + if (rebuildStringActivities) + { + _removeOldActivities(); + + ActivityList.Clear(); + ActivityList.AddRange(Activities + .OrderByDescending(x => x.DateTime) + .Select(x => ((Activity)x).ToString()) + .Distinct() + .Select(x => Database.CryptographyCenter.EncryptAsymmetrically(x, PublicKey))); + } + + Database.FileLocker.Save(this, Database.ActivityFileEntry); + } + + private void _removeOldActivities() + { + if (Database.User is null + || Database.User.NumberOfMonthActivitiesToKeep == 0) + { + return; + } + + DateTime limitDate = DateTime.Now.AddMonths(-Database.User.NumberOfMonthActivitiesToKeep).Date.AddDays(-DateTime.Now.Day + 1); + Activities = [.. Activities.Where(x => x.DateTime >= limitDate || x.NeedsReview)]; + } + } +} diff --git a/Core/Utils/CryptographyCenter.cs b/Core/Utils/CryptographyCenter.cs new file mode 100644 index 0000000..3798ccb --- /dev/null +++ b/Core/Utils/CryptographyCenter.cs @@ -0,0 +1,274 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Upsilon.Apps.Passkey.Interfaces.Utils; + +namespace Upsilon.Apps.Passkey.Core.Utils +{ + public class CryptographyCenter : ICryptographyCenter + { + public string GetHash(string source) + { + string md5Hash = Convert.ToBase64String(MD5.HashData(Encoding.Unicode.GetBytes(source))); + string sha1Hash = Convert.ToBase64String(SHA1.HashData(Encoding.Unicode.GetBytes(source))); + + return (md5Hash + sha1Hash).Replace("/", "-"); + } + + public string GetSlowHash(string source) + { + long realTimeFactor = (long)Math.Pow(0b1001, 6); + + for (int i = 0; i < realTimeFactor; i++) + { + source = GetHash(source); + } + + return source; + } + + public int HashLength => GetHash(string.Empty).Length; + + public void Sign(ref string source) + { + source = GetHash(source) + source; + } + + public bool CheckSign(ref string source) + { + try + { + string hashSource = source[..HashLength]; + string hashCheck = GetHash(source[HashLength..]); + + if (hashSource != hashCheck) + { + throw new Exception(); + } + + source = source[HashLength..]; + } + catch + { + return false; + } + + return true; + } + + public string EncryptSymmetrically(string source, string[] passwords) + { + source = _encryptAes(source, passwords); + source = Convert.ToBase64String(Encoding.Unicode.GetBytes(source)); + + Sign(ref source); + + return source; + } + + public string DecryptSymmetrically(string source, string[] passwords) + { + if (!CheckSign(ref source)) + { + throw new CheckSignFailedException(); + } + + source = Encoding.Unicode.GetString(Convert.FromBase64String(source)); + source = _decryptAes(source, passwords); + + return source; + } + + public void GenerateRandomKeys(out string publicKey, out string privateKey) + { + using RSA rsa = RSA.Create(4096); + + privateKey = rsa.ExportRSAPrivateKeyPem(); + publicKey = rsa.ExportRSAPublicKeyPem(); + } + + public string EncryptAsymmetrically(string source, string key) + { + Random random = new((int)DateTime.Now.Ticks); + byte[] randomBytes = new byte[100]; + random.NextBytes(randomBytes); + string aesKey = Encoding.UTF8.GetString(randomBytes); + source = EncryptSymmetrically(source, [aesKey]); + aesKey = _encryptRsa(aesKey, key); + KeyValuePair s = new(aesKey, source); + source = JsonSerializer.Serialize(s); + + Sign(ref source); + + return source; + } + + public string DecryptAsymmetrically(string source, string key) + { + if (!CheckSign(ref source)) + { + throw new CheckSignFailedException(); + } + + KeyValuePair s = JsonSerializer.Deserialize>(source); + string aesKey = _decryptRsa(s.Key, 0, key); + source = DecryptSymmetrically(s.Value, [aesKey]); + + return source; + } + + private static string _cipherAes(string plainText, string key) + { + if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(plainText)) + { + return plainText; + } + + key = Encoding.ASCII.GetString(MD5.HashData(Encoding.ASCII.GetBytes(key))); + key += Encoding.ASCII.GetString(MD5.HashData(Encoding.ASCII.GetBytes(key))); + key += Encoding.ASCII.GetString(MD5.HashData(Encoding.ASCII.GetBytes(key))); + + byte[] _key = Encoding.ASCII.GetBytes(key[..32]); + byte[] IV = Encoding.ASCII.GetBytes(key.Substring(32, 16)); + + byte[] bytes = _cipherAes(plainText, _key, IV); + + return new string([.. bytes.Select(x => (char)x)]); + } + + private static byte[] _cipherAes(string plainText, byte[] key, byte[] IV) + { + using Aes aesAlg = Aes.Create(); + aesAlg.Key = key; + aesAlg.IV = IV; + + ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); + + using MemoryStream msEncrypt = new(); + using CryptoStream csEncrypt = new(msEncrypt, encryptor, CryptoStreamMode.Write); + using (StreamWriter swEncrypt = new(csEncrypt)) + { + swEncrypt.Write(plainText); + } + + return msEncrypt.ToArray(); + } + + private static string _uncipherAes(string cipherText, string key) + { + if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(cipherText)) + { + return cipherText; + } + key = Encoding.ASCII.GetString(MD5.HashData(Encoding.ASCII.GetBytes(key))); + key += Encoding.ASCII.GetString(MD5.HashData(Encoding.ASCII.GetBytes(key))); + key += Encoding.ASCII.GetString(MD5.HashData(Encoding.ASCII.GetBytes(key))); + + byte[] _key = Encoding.ASCII.GetBytes(key[..32]); + byte[] IV = Encoding.ASCII.GetBytes(key.Substring(32, 16)); + + byte[] bytes = [.. cipherText.Select(x => (byte)x)]; + + return _uncitherAes(bytes, _key, IV); + } + + private static string _uncitherAes(byte[] cipherText, byte[] key, byte[] IV) + { + using Aes aesAlg = Aes.Create(); + aesAlg.Key = key; + aesAlg.IV = IV; + + ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); + + using MemoryStream msDecrypt = new(cipherText); + using CryptoStream csDecrypt = new(msDecrypt, decryptor, CryptoStreamMode.Read); + using StreamReader srDecrypt = new(csDecrypt); + + return srDecrypt.ReadToEnd(); + } + + private string _encryptAes(string source, string[] passwords) + { + passwords = [.. passwords.Select(GetHash)]; + + for (int i = passwords.Length - 1; i >= 0; i--) + { + Sign(ref source); + source = _cipherAes(source, passwords[i]); + } + + Sign(ref source); + source = _cipherAes(source, GetHash(string.Empty)); + + return source; + } + + private string _decryptAes(string source, string[] passwords) + { + passwords = [.. passwords.Select(GetHash)]; + + try + { + source = _uncipherAes(source, GetHash(string.Empty)); + } + catch + { + throw new CorruptedSourceException(); + } + + if (!CheckSign(ref source)) + { + throw new CheckSignFailedException(); + } + + for (int i = 0; i < passwords.Length; i++) + { + try + { + source = _uncipherAes(source, passwords[i]); + + if (!CheckSign(ref source)) + { + throw new CheckSignFailedException(); + } + } + catch + { + throw new WrongPasswordException(i); + } + } + + return source; + } + + private static string _encryptRsa(string source, string publicKeyPem) + { + using RSA rsa = RSA.Create(); + rsa.ImportFromPem(publicKeyPem); + + byte[] bytesPlainTextData = Encoding.Unicode.GetBytes(source); + byte[] bytesCypherText = rsa.Encrypt(bytesPlainTextData, RSAEncryptionPadding.OaepSHA256); + + source = Convert.ToBase64String(bytesCypherText); + + return source; + } + + private static string _decryptRsa(string source, int level, string privateKeyPem) + { + try + { + using RSA rsa = RSA.Create(); + rsa.ImportFromPem(privateKeyPem); + + byte[] bytesCypherText = Convert.FromBase64String(source); + byte[] bytesPlainTextData = rsa.Decrypt(bytesCypherText, RSAEncryptionPadding.OaepSHA256); + return Encoding.Unicode.GetString(bytesPlainTextData); + } + catch + { + throw new WrongPasswordException(level); + } + } + } +} diff --git a/Core/Utils/FileLocker.cs b/Core/Utils/FileLocker.cs new file mode 100644 index 0000000..bac4d8e --- /dev/null +++ b/Core/Utils/FileLocker.cs @@ -0,0 +1,180 @@ +using System.IO.Compression; +using System.Text; +using Upsilon.Apps.Passkey.Interfaces.Utils; + +namespace Upsilon.Apps.Passkey.Core.Utils +{ + internal class FileLocker : IDisposable + { + internal string FilePath { get; private set; } + private FileStream? _stream; + private readonly ICryptographyCenter _cryptographicCenter; + private readonly ISerializationCenter _serializationCenter; + + internal FileLocker(ICryptographyCenter cryptographicCenter, ISerializationCenter serializationCenter, string filePath, FileMode fileMode = FileMode.Open) + { + FilePath = filePath; + + _cryptographicCenter = cryptographicCenter; + _serializationCenter = serializationCenter; + + _stream = new FileStream(FilePath, fileMode, FileAccess.ReadWrite, FileShare.None); + } + + internal void Lock() + { + Unlock(); + + _stream = new FileStream(FilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.Inheritable); + } + + internal void Unlock() + { + if (_stream is null) return; + + _stream.Close(); + _stream.Dispose(); + _stream = null; + } + + internal T Open(string fileEntry, string[] passkeys) where T : notnull + { + return _readContent(fileEntry, passkeys).DeserializeTo(_serializationCenter); + } + + internal T Open(string fileEntry) where T : notnull => Open(fileEntry, []); + + internal void Save(T obj, string fileEntry, string[] passkeys) where T : notnull + { + _writeContent(obj.SerializeWith(_serializationCenter), fileEntry, passkeys); + } + + internal void Save(T obj, string fileEntry) where T : notnull => Save(obj, fileEntry, []); + + internal void Delete() + { + Unlock(); + + if (File.Exists(FilePath)) + { + File.Delete(FilePath); + } + } + + internal void Delete(string fileEntry) + { + Unlock(); + + using (ZipArchive archive = ZipFile.Open(FilePath, ZipArchiveMode.Update, Encoding.UTF8)) + { + ZipArchiveEntry? existingEntry = archive.GetEntry(fileEntry); + existingEntry?.Delete(); + } + + Lock(); + } + + internal bool Exists(string fileEntry) + { + Unlock(); + + bool exists = false; + + using (ZipArchive archive = ZipFile.Open(FilePath, ZipArchiveMode.Update, Encoding.UTF8)) + { + exists = archive.GetEntry(fileEntry) is not null; + } + + Lock(); + + return exists; + } + + public void Dispose() + { + Unlock(); + FilePath = string.Empty; + } + + private static string _compressString(string text) + { + byte[] bytes = Encoding.UTF8.GetBytes(text); + using MemoryStream msi = new(bytes); + using MemoryStream mso = new(); + using (GZipStream gs = new(mso, CompressionLevel.SmallestSize)) + { + msi.CopyTo(gs); + } + return Convert.ToBase64String(mso.ToArray()); + } + + private static string _decompressString(string compressedText) + { + try + { + byte[] bytes = Convert.FromBase64String(compressedText); + using MemoryStream msi = new(bytes); + using MemoryStream mso = new(); + using (GZipStream gs = new(msi, CompressionMode.Decompress)) + { + gs.CopyTo(mso); + } + return Encoding.UTF8.GetString(mso.ToArray()); + } + catch + { + throw new CorruptedSourceException(); + } + } + + private string _readContent(string fileEntry, string[] passkeys) + { + Unlock(); + string content; + + using (ZipArchive archive = ZipFile.OpenRead(FilePath)) + { + ZipArchiveEntry zipEntry = archive.GetEntry(fileEntry) + ?? throw new FileNotFoundException($"The file entry '{fileEntry}' not found in the archive {FilePath}.", $"{FilePath}/{fileEntry}"); + + using Stream stream = zipEntry.Open(); + using StreamReader reader = new(stream, Encoding.UTF8); + + content = passkeys.Length != 0 + ? _cryptographicCenter.DecryptSymmetrically(_decompressString(reader.ReadToEnd()), passkeys) + : _decompressString(reader.ReadToEnd()); + } + + Lock(); + + return content; + } + + private void _writeContent(string content, string fileEntry, string[] passkeys) + { + Unlock(); + + using (ZipArchive archive = ZipFile.Open(FilePath, ZipArchiveMode.Update, Encoding.UTF8)) + { + ZipArchiveEntry? existingEntry = archive.GetEntry(fileEntry); + existingEntry?.Delete(); + + ZipArchiveEntry newEntry = archive.CreateEntry(fileEntry); + + using Stream stream = newEntry.Open(); + using StreamWriter writer = new(stream, Encoding.UTF8); + + if (passkeys.Length != 0) + { + writer.Write(_compressString(_cryptographicCenter.EncryptSymmetrically(content, passkeys))); + } + else + { + writer.Write(_compressString(content)); + } + } + + Lock(); + } + } +} diff --git a/Core/Utils/ImportExportHelper.cs b/Core/Utils/ImportExportHelper.cs new file mode 100644 index 0000000..28705b7 --- /dev/null +++ b/Core/Utils/ImportExportHelper.cs @@ -0,0 +1,199 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Upsilon.Apps.Passkey.Core.Models; +using Upsilon.Apps.Passkey.Interfaces.Enums; +using Upsilon.Apps.Passkey.Interfaces.Models; + +namespace Upsilon.Apps.Passkey.Core.Utils +{ + internal static class ImportExportHelper + { + private enum Headers + { + ServiceName, + ServiceUrl, + ServiceNotes, + AccountLabel, + Identifiers, + Password, + AccountNotes, + AccountOptions, + PasswordUpdateReminderDelay, + } + + private static string _jsonSerialize(T obj) + => JsonSerializer.Serialize(obj, _options); + + private static T _jsonDeserializeAs(string json) + => JsonSerializer.Deserialize(json, _options) ?? throw new NullReferenceException(); + + private static readonly JsonSerializerOptions _options = new() { Converters = { new JsonStringEnumConverter() }, WriteIndented = true, }; + + public static string ImportCSV(this IDatabase database, string importContent) + { + List services = []; + + try + { + string[] csvLines = [.. importContent.Split('\n').Select(x => x.Replace("\r", "")).Where(x => !string.IsNullOrWhiteSpace(x))]; + + string[] headers = csvLines[0].Split("\t"); + + Dictionary headersIndexes = []; + + headersIndexes[Headers.ServiceName] = headers.IndexOf(Headers.ServiceName.ToString()); + headersIndexes[Headers.ServiceUrl] = headers.IndexOf(Headers.ServiceUrl.ToString()); + headersIndexes[Headers.ServiceNotes] = headers.IndexOf(Headers.ServiceNotes.ToString()); + headersIndexes[Headers.AccountLabel] = headers.IndexOf(Headers.AccountLabel.ToString()); + headersIndexes[Headers.Identifiers] = headers.IndexOf(Headers.Identifiers.ToString()); + headersIndexes[Headers.Password] = headers.IndexOf(Headers.Password.ToString()); + headersIndexes[Headers.AccountNotes] = headers.IndexOf(Headers.AccountNotes.ToString()); + headersIndexes[Headers.AccountOptions] = headers.IndexOf(Headers.AccountOptions.ToString()); + headersIndexes[Headers.PasswordUpdateReminderDelay] = headers.IndexOf(Headers.PasswordUpdateReminderDelay.ToString()); + + if (headersIndexes.Values.Any(x => x == -1)) return $"the CSV headers should be : {string.Join(", ", headersIndexes.Keys.Select(x => $"'{x}'"))}"; + + Service? service = null; + + for (int i = 1; i < csvLines.Length; i++) + { + string csvLine = csvLines[i]; + string[] csvColumns = csvLine.Split('\t'); + string serviceName = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.ServiceName]]); + string serviceUrl = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.ServiceUrl]]); + string serviceNotes = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.ServiceNotes]]); + string accountLabel = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.AccountLabel]]); + string identifiers = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.Identifiers]]); + string password = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.Password]]); + string accountNotes = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.AccountNotes]]); + AccountOption accountOptions = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.AccountOptions]]); + int passwordUpdateReminderDelay = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.PasswordUpdateReminderDelay]]); + + if (service is null + || service.ServiceName != serviceName) + { + service = new() + { + ServiceName = serviceName, + Url = serviceUrl, + Notes = serviceNotes, + }; + + services.Add(service); + } + + Account account = new() + { + Label = accountLabel, + Identifiers = [.. identifiers.Split('|').Select(x => x.Trim())], + Password = password, + Notes = accountNotes, + Options = accountOptions, + PasswordUpdateReminderDelay = passwordUpdateReminderDelay + }; + + service.Accounts.Add(account); + } + } + catch + { + return "the CSV data format is incorrect"; + } + + return _importServices(database, services); + } + + public static string ImportJson(this IDatabase database, string importContent) + { + Service[] services; + + try + { + services = _jsonDeserializeAs(importContent); + } + catch + { + return "import file deserialization failed"; + } + + return _importServices(database, services); + } + + private static string _importServices(IDatabase database, IEnumerable services) + { + if (database.User is null) return string.Empty; + + if (!services.Any()) return "there is no data to import"; + + Service? s0 = services.FirstOrDefault(x => database.User.Services.Any(y => y.ServiceName == x.ServiceName)); + if (s0 is not null) + { + return $"service '{s0.ServiceName}' already exists"; + } + + s0 = services.FirstOrDefault(x => string.IsNullOrWhiteSpace(x.ServiceName)); + if (s0 is not null) + { + return $"service name cannot be blank"; + } + + foreach (Service s in services) + { + IService service = database.User.AddService(s.ServiceName); + service.Url = s.Url; + service.Notes = s.Notes; + + foreach (Account a in s.Accounts) + { + IAccount account = service.AddAccount(a.Label, a.Identifiers, a.Password); + account.Notes = a.Notes; + account.Options = a.Options; + account.PasswordUpdateReminderDelay = a.PasswordUpdateReminderDelay; + } + } + + return string.Empty; + } + + public static string ExportCSV(this Database database, string filePath) + { + if (database.User is null) return string.Empty; + + StringBuilder sb = new(string.Join("\t", Enum.GetNames()) + "\n"); + + foreach (Service service in database.User.Services) + { + string serviceLine = $"{_jsonSerialize(service.ServiceName.Trim())}\t" + + $"{_jsonSerialize(service.Url.Trim())}\t" + + $"{_jsonSerialize(service.Notes.Trim())}\t"; + + foreach (Account account in service.Accounts) + { + string identifiers = string.Join("|", account.Identifiers.Where(x => !string.IsNullOrWhiteSpace(x))); + + _ = sb.Append(serviceLine); + _ = sb.Append($"{_jsonSerialize(account.Label.Trim())}\t" + + $"{_jsonSerialize(identifiers)}\t" + + $"{_jsonSerialize(account.Password.Trim())}\t" + + $"{_jsonSerialize(account.Notes.Trim())}\t" + + $"{_jsonSerialize(account.Options)}\t" + + $"{_jsonSerialize(account.PasswordUpdateReminderDelay)}\n"); + } + } + + File.WriteAllText(filePath, sb.ToString()); + + return string.Empty; + } + + public static string ExportJson(this Database database, string filePath) + { + if (database.User is null) return string.Empty; + + File.WriteAllText(filePath, _jsonSerialize(database.User.Services)); + + return string.Empty; + } + } +} diff --git a/Core/Utils/JsonSerializationCenter.cs b/Core/Utils/JsonSerializationCenter.cs new file mode 100644 index 0000000..518ce3d --- /dev/null +++ b/Core/Utils/JsonSerializationCenter.cs @@ -0,0 +1,17 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Upsilon.Apps.Passkey.Interfaces.Utils; + +namespace Upsilon.Apps.Passkey.Core.Utils +{ + public class JsonSerializationCenter : ISerializationCenter + { + private static readonly JsonSerializerOptions _options = new() { Converters = { new JsonStringEnumConverter() }, }; + + public string Serialize(T toSerialize) where T : notnull + => JsonSerializer.Serialize(toSerialize, _options); + + public T Deserialize(string toDeserialize) where T : notnull + => JsonSerializer.Deserialize(toDeserialize, _options) ?? throw new NullReferenceException(nameof(toDeserialize)); + } +} diff --git a/Core/Utils/PasswordFactory.cs b/Core/Utils/PasswordFactory.cs new file mode 100644 index 0000000..b178b77 --- /dev/null +++ b/Core/Utils/PasswordFactory.cs @@ -0,0 +1,54 @@ +using System.Security.Cryptography; +using System.Text; +using Upsilon.Apps.Passkey.Interfaces.Utils; + +namespace Upsilon.Apps.Passkey.Core.Utils +{ + public class PasswordFactory : IPasswordFactory + { + public string Alphabetic => "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + public string Numeric => "0123456789"; + public string SpecialChars => "~!@#$%^&*()_-+={[}]\\|'\";:,<.>/?"; + + public string GeneratePassword(int length, string alphabet, bool checkIfLeaked = true) + { + if (string.IsNullOrWhiteSpace(alphabet) + || length == 0) + { + return string.Empty; + } + + StringBuilder stringBuilder = new(); + Random random = new((int)DateTime.Now.Ticks); + byte iteration = 0; + + do + { + iteration++; + _ = stringBuilder.Clear(); + + for (int i = 0; i < length; i++) + { + _ = stringBuilder.Append(alphabet[random.Next(alphabet.Length)]); + } + } + while (iteration < length && checkIfLeaked && PasswordLeaked(stringBuilder.ToString())); + + return iteration == length ? string.Empty : stringBuilder.ToString(); + } + + public bool PasswordLeaked(string password) + { + string hash = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(password))); + + using HttpClient httpClient = new(); + HttpRequestMessage request = new(HttpMethod.Get, $"https://api.pwnedpasswords.com/range/{hash[..5]}"); + HttpResponseMessage response = httpClient.Send(request); + using StreamReader reader = new(response.Content.ReadAsStream()); + + string res = reader.ReadToEnd(); + + return res.Contains(hash[5..]); + } + } +} diff --git a/Core/Utils/QrCode.cs b/Core/Utils/QrCode.cs new file mode 100644 index 0000000..aec87ef --- /dev/null +++ b/Core/Utils/QrCode.cs @@ -0,0 +1,1041 @@ +using System.Text; + +namespace Upsilon.Apps.Passkey.Core.Utils; + +public enum ErrorCorrection +{ + L, + M, + Q, + H, +} + +internal enum EncodingMode +{ + Terminator, + Numeric, + AlphaNumeric, + Append, + Byte, + FNC1First, + Unknown6, + ECI, + Kanji, + FNC1Second, + Unknown10, + Unknown11, + Unknown12, + Unknown13, + Unknown14, + Unknown15, +} + +public class QrCode +{ + private byte[][] _dataSegArray = []; + private int _encodedDataBits; + private int _maxCodewords; + private int _maxDataCodewords; + private int _maxDataBits; + private int _errCorrCodewords; + private int _blocksGroup1; + private int _dataCodewordsGroup1; + private int _blocksGroup2; + private int _dataCodewordsGroup2; + private int _maskCode; + private EncodingMode[] _encodingSegMode = []; + private byte[] _codewordsArray = []; + private int _codewordsPtr; + private uint _bitBuffer; + private int _bitBufferLen; + private byte[,] _baseMatrix = new byte[0, 0]; + private byte[,] _maskMatrix = new byte[0, 0]; + private byte[,] _resultMatrix = new byte[0, 0]; + internal static readonly byte[]?[] AlignmentPositionArray = [null, null, [6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50], [6, 30, 54], [6, 32/*0x20*/, 58], [6, 34, 62], [6, 26, 46, 66], [6, 26, 48/*0x30*/, 70], [6, 26, 50, 74], [6, 30, 54, 78], [6, 30, 56, 82], [6, 30, 58, 86], [6, 34, 62, 90], [6, 28, 50, 72, 94], [6, 26, 50, 74, 98], [6, 30, 54, 78, 102], [6, 28, 54, 80/*0x50*/, 106], [6, 32/*0x20*/, 58, 84, 110], [6, 30, 58, 86, 114], [6, 34, 62, 90, 118], [6, 26, 50, 74, 98, 122], [6, 30, 54, 78, 102, 126], [6, 26, 52, 78, 104, 130], [6, 30, 56, 82, 108, 134], [6, 34, 60, 86, 112/*0x70*/, 138], [6, 30, 58, 86, 114, 142], [6, 34, 62, 90, 118, 146], [6, 30, 54, 78, 102, 126, 150], [6, 24, 50, 76, 102, 128/*0x80*/, 154], [6, 28, 54, 80/*0x50*/, 106, 132, 158], [6, 32/*0x20*/, 58, 84, 110, 136, 162], [6, 26, 54, 82, 110, 138, 166], [6, 30, 58, 86, 114, 142, 170]]; + internal static readonly int[] MaxCodewordsArray = [0, 26, 44, 70, 100, 134, 172, 196, 242, 292, 346, 404, 466, 532, 581, 655, 733, 815, 901, 991, 1085, 1156, 1258, 1364, 1474, 1588, 1706, 1828, 1921, 2051, 2185, 2323, 2465, 2611, 2761, 2876, 3034, 3196, 3362, 3532, 3706]; + internal static readonly byte[] EncodingTable = [45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 36, 45, 45, 45, 37, 38, 45, 45, 45, 45, 39, 40, 45, 41, 42, 43, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, 45, 45, 45, 45, 45, 45, 10, 11, 12, 13, 14, 15, 16/*0x10*/, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31/*0x1F*/, 32/*0x20*/, 33, 34, 35, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45]; + internal const int BLOCKS_GROUP1 = 0; + internal const int DATA_CODEWORDS_GROUP1 = 1; + internal const int BLOCKS_GROUP2 = 2; + internal const int DATA_CODEWORDS_GROUP2 = 3; + internal static readonly byte[,] ECBlockInfo = new byte[160/*0xA0*/, 4] { { 1, 19, 0, 0 }, { 1, 16/*0x10*/, 0, 0 }, { 1, 13, 0, 0 }, { 1, 9, 0, 0 }, { 1, 34, 0, 0 }, { 1, 28, 0, 0 }, { 1, 22, 0, 0 }, { 1, 16/*0x10*/, 0, 0 }, { 1, 55, 0, 0 }, { 1, 44, 0, 0 }, { 2, 17, 0, 0 }, { 2, 13, 0, 0 }, { 1, 80/*0x50*/, 0, 0 }, { 2, 32/*0x20*/, 0, 0 }, { 2, 24, 0, 0 }, { 4, 9, 0, 0 }, { 1, 108, 0, 0 }, { 2, 43, 0, 0 }, { 2, 15, 2, 16/*0x10*/}, { 2, 11, 2, 12 }, { 2, 68, 0, 0 }, { 4, 27, 0, 0 }, { 4, 19, 0, 0 }, { 4, 15, 0, 0 }, { 2, 78, 0, 0 }, { 4, 31/*0x1F*/, 0, 0 }, { 2, 14, 4, 15 }, { 4, 13, 1, 14 }, { 2, 97, 0, 0 }, { 2, 38, 2, 39 }, { 4, 18, 2, 19 }, { 4, 14, 2, 15 }, { 2, 116, 0, 0 }, { 3, 36, 2, 37 }, { 4, 16/*0x10*/, 4, 17 }, { 4, 12, 4, 13 }, { 2, 68, 2, 69 }, { 4, 43, 1, 44 }, { 6, 19, 2, 20 }, { 6, 15, 2, 16/*0x10*/}, { 4, 81, 0, 0 }, { 1, 50, 4, 51 }, { 4, 22, 4, 23 }, { 3, 12, 8, 13 }, { 2, 92, 2, 93 }, { 6, 36, 2, 37 }, { 4, 20, 6, 21 }, { 7, 14, 4, 15 }, { 4, 107, 0, 0 }, { 8, 37, 1, 38 }, { 8, 20, 4, 21 }, { 12, 11, 4, 12 }, { 3, 115, 1, 116 }, { 4, 40, 5, 41 }, { 11, 16/*0x10*/, 5, 17 }, { 11, 12, 5, 13 }, { 5, 87, 1, 88 }, { 5, 41, 5, 42 }, { 5, 24, 7, 25 }, { 11, 12, 7, 13 }, { 5, 98, 1, 99 }, { 7, 45, 3, 46 }, { 15, 19, 2, 20 }, { 3, 15, 13, 16/*0x10*/}, { 1, 107, 5, 108 }, { 10, 46, 1, 47 }, { 1, 22, 15, 23 }, { 2, 14, 17, 15 }, { 5, 120, 1, 121 }, { 9, 43, 4, 44 }, { 17, 22, 1, 23 }, { 2, 14, 19, 15 }, { 3, 113, 4, 114 }, { 3, 44, 11, 45 }, { 17, 21, 4, 22 }, { 9, 13, 16/*0x10*/, 14 }, { 3, 107, 5, 108 }, { 3, 41, 13, 42 }, { 15, 24, 5, 25 }, { 15, 15, 10, 16/*0x10*/}, { 4, 116, 4, 117 }, { 17, 42, 0, 0 }, { 17, 22, 6, 23 }, { 19, 16/*0x10*/, 6, 17 }, { 2, 111, 7, 112/*0x70*/}, { 17, 46, 0, 0 }, { 7, 24, 16/*0x10*/, 25 }, { 34, 13, 0, 0 }, { 4, 121, 5, 122 }, { 4, 47, 14, 48/*0x30*/}, { 11, 24, 14, 25 }, { 16/*0x10*/, 15, 14, 16/*0x10*/}, { 6, 117, 4, 118 }, { 6, 45, 14, 46 }, { 11, 24, 16/*0x10*/, 25 }, { 30, 16/*0x10*/, 2, 17 }, { 8, 106, 4, 107 }, { 8, 47, 13, 48/*0x30*/}, { 7, 24, 22, 25 }, { 22, 15, 13, 16/*0x10*/}, { 10, 114, 2, 115 }, { 19, 46, 4, 47 }, { 28, 22, 6, 23 }, { 33, 16/*0x10*/, 4, 17 }, { 8, 122, 4, 123 }, { 22, 45, 3, 46 }, { 8, 23, 26, 24 }, { 12, 15, 28, 16/*0x10*/}, { 3, 117, 10, 118 }, { 3, 45, 23, 46 }, { 4, 24, 31/*0x1F*/, 25 }, { 11, 15, 31/*0x1F*/, 16/*0x10*/}, { 7, 116, 7, 117 }, { 21, 45, 7, 46 }, { 1, 23, 37, 24 }, { 19, 15, 26, 16/*0x10*/}, { 5, 115, 10, 116 }, { 19, 47, 10, 48/*0x30*/}, { 15, 24, 25, 25 }, { 23, 15, 25, 16/*0x10*/}, { 13, 115, 3, 116 }, { 2, 46, 29, 47 }, { 42, 24, 1, 25 }, { 23, 15, 28, 16/*0x10*/}, { 17, 115, 0, 0 }, { 10, 46, 23, 47 }, { 10, 24, 35, 25 }, { 19, 15, 35, 16/*0x10*/}, { 17, 115, 1, 116 }, { 14, 46, 21, 47 }, { 29, 24, 19, 25 }, { 11, 15, 46, 16/*0x10*/}, { 13, 115, 6, 116 }, { 14, 46, 23, 47 }, { 44, 24, 7, 25 }, { 59, 16/*0x10*/, 1, 17 }, { 12, 121, 7, 122 }, { 12, 47, 26, 48/*0x30*/}, { 39, 24, 14, 25 }, { 22, 15, 41, 16/*0x10*/}, { 6, 121, 14, 122 }, { 6, 47, 34, 48/*0x30*/}, { 46, 24, 10, 25 }, { 2, 15, 64/*0x40*/, 16/*0x10*/}, { 17, 122, 4, 123 }, { 29, 46, 14, 47 }, { 49, 24, 10, 25 }, { 24, 15, 46, 16/*0x10*/}, { 4, 122, 18, 123 }, { 13, 46, 32/*0x20*/, 47 }, { 48/*0x30*/, 24, 14, 25 }, { 42, 15, 32/*0x20*/, 16/*0x10*/}, { 20, 117, 4, 118 }, { 40, 47, 7, 48/*0x30*/}, { 43, 24, 22, 25 }, { 10, 15, 67, 16/*0x10*/}, { 19, 118, 6, 119 }, { 18, 47, 31/*0x1F*/, 48/*0x30*/}, { 34, 24, 34, 25 }, { 20, 15, 61, 16/*0x10*/} }; + private static readonly byte[] _generator7 = [87, 229, 146, 149, 238, 102, 21]; + private static readonly byte[] _generator10 = [251, 67, 46, 61, 118, 70, 64 /*0x40*/, 94, 32 /*0x20*/, 45]; + private static readonly byte[] _generator13 = [74, 152, 176 /*0xB0*/, 100, 86, 100, 106, 104, 130, 218, 206, 140, 78]; + private static readonly byte[] _generator15 = [8, 183, 61, 91, 202, 37, 51, 58, 58, 237, 140, 124, 5, 99, 105]; + private static readonly byte[] _generator16 = [120, 104, 107, 109, 102, 161, 76, 3, 91, 191, 147, 169, 182, 194, 225, 120]; + private static readonly byte[] _generator17 = [43, 139, 206, 78, 43, 239, 123, 206, 214, 147, 24, 99, 150, 39, 243, 163, 136]; + private static readonly byte[] _generator18 = [215, 234, 158, 94, 184, 97, 118, 170, 79, 187, 152, 148, 252, 179, 5, 98, 96 /*0x60*/, 153]; + private static readonly byte[] _generator20 = [17, 60, 79, 50, 61, 163, 26, 187, 202, 180, 221, 225, 83, 239, 156, 164, 212, 212, 188, 190]; + private static readonly byte[] _generator22 = [210, 171, 247, 242, 93, 230, 14, 109, 221, 53, 200, 74, 8, 172, 98, 80 /*0x50*/, 219, 134, 160 /*0xA0*/, 105, 165, 231]; + private static readonly byte[] _generator24 = [229, 121, 135, 48 /*0x30*/, 211, 117, 251, 126, 159, 180, 169, 152, 192 /*0xC0*/, 226, 228, 218, 111, 0, 117, 232, 87, 96 /*0x60*/, 227, 21]; + private static readonly byte[] _generator26 = [173, 125, 158, 2, 103, 182, 118, 17, 145, 201, 111, 28, 165, 53, 161, 21, 245, 142, 13, 102, 48 /*0x30*/, 227, 153, 145, 218, 70]; + private static readonly byte[] _generator28 = [168, 223, 200, 104, 224 /*0xE0*/, 234, 108, 180, 110, 190, 195, 147, 205, 27, 232, 201, 21, 43, 245, 87, 42, 195, 212, 119, 242, 37, 9, 123]; + private static readonly byte[] _generator30 = [41, 173, 145, 152, 216, 31 /*0x1F*/, 179, 182, 50, 48 /*0x30*/, 110, 86, 239, 96 /*0x60*/, 222, 125, 42, 173, 226, 193, 224 /*0xE0*/, 130, 156, 37, 251, 216, 238, 40, 192 /*0xC0*/, 180]; + private static readonly byte[] _generator32 = [10, 6, 106, 190, 249, 167, 4, 67, 209, 138, 138, 32 /*0x20*/, 242, 123, 89, 27, 120, 185, 80 /*0x50*/, 156, 38, 60, 171, 60, 28, 222, 80 /*0x50*/, 52, 254, 185, 220, 241]; + private static readonly byte[] _generator34 = [111, 77, 146, 94, 26, 21, 108, 19, 105, 94, 113, 193, 86, 140, 163, 125, 58, 158, 229, 239, 218, 103, 56, 70, 114, 61, 183, 129, 167, 13, 98, 62, 129, 51]; + private static readonly byte[] _generator36 = [200, 183, 98, 16 /*0x10*/, 172, 31 /*0x1F*/, 246, 234, 60, 152, 115, 0, 167, 152, 113, 248, 238, 107, 18, 63 /*0x3F*/, 218, 37, 87, 210, 105, 177, 120, 74, 121, 196, 117, 251, 113, 233, 30, 120]; + private static readonly byte[] _generator40 = [59, 116, 79, 161, 252, 98, 128 /*0x80*/, 205, 128 /*0x80*/, 161, 247, 57, 163, 56, 235, 106, 53, 26, 187, 174, 226, 104, 170, 7, 175, 35, 181, 114, 88, 41, 47, 163, 125, 134, 72, 20, 232, 53, 35, 15]; + private static readonly byte[] _generator42 = [250, 103, 221, 230, 25, 18, 137, 231, 0, 3, 58, 242, 221, 191, 110, 84, 230, 8, 188, 106, 96 /*0x60*/, 147, 15, 131, 139, 34, 101, 223, 39, 101, 213, 199, 237, 254, 201, 123, 171, 162, 194, 117, 50, 96 /*0x60*/ ]; + private static readonly byte[] _generator44 = [190, 7, 61, 121, 71, 246, 69, 55, 168, 188, 89, 243, 191, 25, 72, 123, 9, 145, 14, 247, 1, 238, 44, 78, 143, 62, 224 /*0xE0*/, 126, 118, 114, 68, 163, 52, 194, 217, 147, 204, 169, 37, 130, 113, 102, 73, 181]; + private static readonly byte[] _generator46 = [112 /*0x70*/, 94, 88, 112 /*0x70*/, 253, 224 /*0xE0*/, 202, 115, 187, 99, 89, 5, 54, 113, 129, 44, 58, 16 /*0x10*/, 135, 216, 169, 211, 36, 1, 4, 96 /*0x60*/, 60, 241, 73, 104, 234, 8, 249, 245, 119, 174, 52, 25, 157, 224 /*0xE0*/, 43, 202, 223, 19, 82, 15]; + private static readonly byte[] _generator48 = [228, 25, 196, 130, 211, 146, 60, 24, 251, 90, 39, 102, 240 /*0xF0*/, 61, 178, 63 /*0x3F*/, 46, 123, 115, 18, 221, 111, 135, 160 /*0xA0*/, 182, 205, 107, 206, 95, 150, 120, 184, 91, 21, 247, 156, 140, 238, 191, 11, 94, 227, 84, 50, 163, 39, 34, 108]; + private static readonly byte[] _generator50 = [232, 125, 157, 161, 164, 9, 118, 46, 209, 99, 203, 193, 35, 3, 209, 111, 195, 242, 203, 225, 46, 13, 32 /*0x20*/, 160 /*0xA0*/, 126, 209, 130, 160 /*0xA0*/, 242, 215, 242, 75, 77, 42, 189, 32 /*0x20*/, 113, 65, 124, 69, 228, 114, 235, 175, 124, 170, 215, 232, 133, 205]; + private static readonly byte[] _generator52 = [116, 50, 86, 186, 50, 220, 251, 89, 192 /*0xC0*/, 46, 86, 127 /*0x7F*/, 124, 19, 184, 233, 151, 215, 22, 14, 59, 145, 37, 242, 203, 134, 254, 89, 190, 94, 59, 65, 124, 113, 100, 233, 235, 121, 22, 76, 86, 97, 39, 242, 200, 220, 101, 33, 239, 254, 116, 51]; + private static readonly byte[] _generator54 = [183, 26, 201, 84, 210, 221, 113, 21, 46, 65, 45, 50, 238, 184, 249, 225, 102, 58, 209, 218, 109, 165, 26, 95, 184, 192 /*0xC0*/, 52, 245, 35, 254, 238, 175, 172, 79, 123, 25, 122, 43, 120, 108, 215, 80 /*0x50*/, 128 /*0x80*/, 201, 235, 8, 153, 59, 101, 31 /*0x1F*/, 198, 76, 31 /*0x1F*/, 156]; + private static readonly byte[] _generator56 = [106, 120, 107, 157, 164, 216, 112 /*0x70*/, 116, 2, 91, 248, 163, 36, 201, 202, 229, 6, 144 /*0x90*/, 254, 155, 135, 208 /*0xD0*/, 170, 209, 12, 139, 127 /*0x7F*/, 142, 182, 249, 177, 174, 190, 28, 10, 85, 239, 184, 101, 124, 152, 206, 96 /*0x60*/, 23, 163, 61, 27, 196, 247, 151, 154, 202, 207, 20, 61, 10]; + private static readonly byte[] _generator58 = [82, 116, 26, 247, 66, 27, 62, 107, 252, 182, 200, 185, 235, 55, 251, 242, 210, 144 /*0x90*/, 154, 237, 176 /*0xB0*/, 141, 192 /*0xC0*/, 248, 152, 249, 206, 85, 253, 142, 65, 165, 125, 23, 24, 30, 122, 240 /*0xF0*/, 214, 6, 129, 218, 29, 145, 127 /*0x7F*/, 134, 206, 245, 117, 29, 41, 63 /*0x3F*/, 159, 142, 233, 125, 148, 123]; + private static readonly byte[] _generator60 = [107, 140, 26, 12, 9, 141, 243, 197, 226, 197, 219, 45, 211, 101, 219, 120, 28, 181, 127 /*0x7F*/, 6, 100, 247, 2, 205, 198, 57, 115, 219, 101, 109, 160 /*0xA0*/, 82, 37, 38, 238, 49, 160 /*0xA0*/, 209, 121, 86, 11, 124, 30, 181, 84, 25, 194, 87, 65, 102, 190, 220, 70, 27, 209, 16 /*0x10*/, 89, 7, 33, 240 /*0xF0*/ ]; + private static readonly byte[] _generator62 = [65, 202, 113, 98, 71, 223, 248, 118, 214, 94, 0, 122, 37, 23, 2, 228, 58, 121, 7, 105, 135, 78, 243, 118, 70, 76, 223, 89, 72, 50, 70, 111, 194, 17, 212, 126, 181, 35, 221, 117, 235, 11, 229, 149, 147, 123, 213, 40, 115, 6, 200, 100, 26, 246, 182, 218, 127 /*0x7F*/, 215, 36, 186, 110, 106]; + private static readonly byte[] _generator64 = [45, 51, 175, 9, 7, 158, 159, 49, 68, 119, 92, 123, 177, 204, 187, 254, 200, 78, 141, 149, 119, 26, 127 /*0x7F*/, 53, 160 /*0xA0*/, 93, 199, 212, 29, 24, 145, 156, 208 /*0xD0*/, 150, 218, 209, 4, 216, 91, 47, 184, 146, 47, 140, 195, 195, 125, 242, 238, 63 /*0x3F*/, 99, 108, 140, 230, 242, 31 /*0x1F*/, 204, 11, 178, 243, 217, 156, 213, 231]; + private static readonly byte[] _generator66 = [5, 118, 222, 180, 136, 136, 162, 51, 46, 117, 13, 215, 81, 17, 139, 247, 197, 171, 95, 173, 65, 137, 178, 68, 111, 95, 101, 41, 72, 214, 169, 197, 95, 7, 44, 154, 77, 111, 236, 40, 121, 143, 63 /*0x3F*/, 87, 80 /*0x50*/, 253, 240 /*0xF0*/, 126, 217, 77, 34, 232, 106, 50, 168, 82, 76, 146, 67, 106, 171, 25, 132, 93, 45, 105]; + private static readonly byte[] _generator68 = [247, 159, 223, 33, 224 /*0xE0*/, 93, 77, 70, 90, 160 /*0xA0*/, 32 /*0x20*/, 254, 43, 150, 84, 101, 190, 205, 133, 52, 60, 202, 165, 220, 203, 151, 93, 84, 15, 84, 253, 173, 160 /*0xA0*/, 89, 227, 52, 199, 97, 95, 231, 52, 177, 41, 125, 137, 241, 166, 225, 118, 2, 54, 32 /*0x20*/, 82, 215, 175, 198, 43, 238, 235, 27, 101, 184, 127 /*0x7F*/, 3, 5, 8, 163, 238]; + internal static readonly byte[]?[] GenArray = [_generator7, null, null, _generator10, null, null, _generator13, null, _generator15, _generator16, _generator17, _generator18, null, _generator20, null, _generator22, null, _generator24, null, _generator26, null, _generator28, null, _generator30, null, _generator32, null, _generator34, null, _generator36, null, null, null, _generator40, null, _generator42, null, _generator44, null, _generator46, null, _generator48, null, _generator50, null, _generator52, null, _generator54, null, _generator56, null, _generator58, null, _generator60, null, _generator62, null, _generator64, null, _generator66, null, _generator68]; + internal static readonly byte[] ExpToInt = [1, 2, 4, 8, 16 /*0x10*/, 32 /*0x20*/, 64 /*0x40*/, 128 /*0x80*/, 29, 58, 116, 232, 205, 135, 19, 38, 76, 152, 45, 90, 180, 117, 234, 201, 143, 3, 6, 12, 24, 48 /*0x30*/, 96 /*0x60*/, 192 /*0xC0*/, 157, 39, 78, 156, 37, 74, 148, 53, 106, 212, 181, 119, 238, 193, 159, 35, 70, 140, 5, 10, 20, 40, 80 /*0x50*/, 160 /*0xA0*/, 93, 186, 105, 210, 185, 111, 222, 161, 95, 190, 97, 194, 153, 47, 94, 188, 101, 202, 137, 15, 30, 60, 120, 240 /*0xF0*/, 253, 231, 211, 187, 107, 214, 177, 127 /*0x7F*/, 254, 225, 223, 163, 91, 182, 113, 226, 217, 175, 67, 134, 17, 34, 68, 136, 13, 26, 52, 104, 208 /*0xD0*/, 189, 103, 206, 129, 31 /*0x1F*/, 62, 124, 248, 237, 199, 147, 59, 118, 236, 197, 151, 51, 102, 204, 133, 23, 46, 92, 184, 109, 218, 169, 79, 158, 33, 66, 132, 21, 42, 84, 168, 77, 154, 41, 82, 164, 85, 170, 73, 146, 57, 114, 228, 213, 183, 115, 230, 209, 191, 99, 198, 145, 63 /*0x3F*/, 126, 252, 229, 215, 179, 123, 246, 241, byte.MaxValue, 227, 219, 171, 75, 150, 49, 98, 196, 149, 55, 110, 220, 165, 87, 174, 65, 130, 25, 50, 100, 200, 141, 7, 14, 28, 56, 112 /*0x70*/, 224 /*0xE0*/, 221, 167, 83, 166, 81, 162, 89, 178, 121, 242, 249, 239, 195, 155, 43, 86, 172, 69, 138, 9, 18, 36, 72, 144 /*0x90*/, 61, 122, 244, 245, 247, 243, 251, 235, 203, 139, 11, 22, 44, 88, 176 /*0xB0*/, 125, 250, 233, 207, 131, 27, 54, 108, 216, 173, 71, 142, 1, 2, 4, 8, 16 /*0x10*/, 32 /*0x20*/, 64 /*0x40*/, 128 /*0x80*/, 29, 58, 116, 232, 205, 135, 19, 38, 76, 152, 45, 90, 180, 117, 234, 201, 143, 3, 6, 12, 24, 48 /*0x30*/, 96 /*0x60*/, 192 /*0xC0*/, 157, 39, 78, 156, 37, 74, 148, 53, 106, 212, 181, 119, 238, 193, 159, 35, 70, 140, 5, 10, 20, 40, 80 /*0x50*/, 160 /*0xA0*/, 93, 186, 105, 210, 185, 111, 222, 161, 95, 190, 97, 194, 153, 47, 94, 188, 101, 202, 137, 15, 30, 60, 120, 240 /*0xF0*/, 253, 231, 211, 187, 107, 214, 177, 127 /*0x7F*/, 254, 225, 223, 163, 91, 182, 113, 226, 217, 175, 67, 134, 17, 34, 68, 136, 13, 26, 52, 104, 208 /*0xD0*/, 189, 103, 206, 129, 31 /*0x1F*/, 62, 124, 248, 237, 199, 147, 59, 118, 236, 197, 151, 51, 102, 204, 133, 23, 46, 92, 184, 109, 218, 169, 79, 158, 33, 66, 132, 21, 42, 84, 168, 77, 154, 41, 82, 164, 85, 170, 73, 146, 57, 114, 228, 213, 183, 115, 230, 209, 191, 99, 198, 145, 63 /*0x3F*/, 126, 252, 229, 215, 179, 123, 246, 241, byte.MaxValue, 227, 219, 171, 75, 150, 49, 98, 196, 149, 55, 110, 220, 165, 87, 174, 65, 130, 25, 50, 100, 200, 141, 7, 14, 28, 56, 112 /*0x70*/, 224 /*0xE0*/, 221, 167, 83, 166, 81, 162, 89, 178, 121, 242, 249, 239, 195, 155, 43, 86, 172, 69, 138, 9, 18, 36, 72, 144 /*0x90*/, 61, 122, 244, 245, 247, 243, 251, 235, 203, 139, 11, 22, 44, 88, 176 /*0xB0*/, 125, 250, 233, 207, 131, 27, 54, 108, 216, 173, 71, 142, 1]; + internal static readonly byte[] IntToExp = [0, 0, 1, 25, 2, 50, 26, 198, 3, 223, 51, 238, 27, 104, 199, 75, 4, 100, 224 /*0xE0*/, 14, 52, 141, 239, 129, 28, 193, 105, 248, 200, 8, 76, 113, 5, 138, 101, 47, 225, 36, 15, 33, 53, 147, 142, 218, 240 /*0xF0*/, 18, 130, 69, 29, 181, 194, 125, 106, 39, 249, 185, 201, 154, 9, 120, 77, 228, 114, 166, 6, 191, 139, 98, 102, 221, 48 /*0x30*/, 253, 226, 152, 37, 179, 16 /*0x10*/, 145, 34, 136, 54, 208 /*0xD0*/, 148, 206, 143, 150, 219, 189, 241, 210, 19, 92, 131, 56, 70, 64 /*0x40*/, 30, 66, 182, 163, 195, 72, 126, 110, 107, 58, 40, 84, 250, 133, 186, 61, 202, 94, 155, 159, 10, 21, 121, 43, 78, 212, 229, 172, 115, 243, 167, 87, 7, 112 /*0x70*/, 192 /*0xC0*/, 247, 140, 128 /*0x80*/, 99, 13, 103, 74, 222, 237, 49, 197, 254, 24, 227, 165, 153, 119, 38, 184, 180, 124, 17, 68, 146, 217, 35, 32 /*0x20*/, 137, 46, 55, 63 /*0x3F*/, 209, 91, 149, 188, 207, 205, 144 /*0x90*/, 135, 151, 178, 220, 252, 190, 97, 242, 86, 211, 171, 20, 42, 93, 158, 132, 60, 57, 83, 71, 109, 65, 162, 31 /*0x1F*/, 45, 67, 216, 183, 123, 164, 118, 196, 23, 73, 236, 127 /*0x7F*/, 12, 111, 246, 108, 161, 59, 82, 41, 157, 85, 170, 251, 96 /*0x60*/, 134, 177, 187, 204, 62, 90, 203, 89, 95, 176 /*0xB0*/, 156, 169, 160 /*0xA0*/, 81, 11, 245, 22, 235, 122, 117, 44, 215, 79, 174, 213, 233, 230, 231, 173, 232, 116, 214, 244, 234, 168, 80 /*0x50*/, 88, 175]; + internal static readonly int[] FormatInfoArray = [21522, 20773, 24188, 23371, 17913, 16590, 20375, 19104, 30660, 29427, 32170, 30877, 26159, 25368, 27713, 26998, 5769, 5054, 7399, 6608, 1890, 597, 3340, 2107, 13663, 12392, 16177, 14854, 9396, 8579, 11994, 11245]; + internal static readonly int[,] FormatInfoOne = new int[15, 2] { { 0, 8 }, { 1, 8 }, { 2, 8 }, { 3, 8 }, { 4, 8 }, { 5, 8 }, { 7, 8 }, { 8, 8 }, { 8, 7 }, { 8, 5 }, { 8, 4 }, { 8, 3 }, { 8, 2 }, { 8, 1 }, { 8, 0 } }; + internal static readonly int[,] FormatInfoTwo = new int[15, 2] { { 8, -1 }, { 8, -2 }, { 8, -3 }, { 8, -4 }, { 8, -5 }, { 8, -6 }, { 8, -7 }, { 8, -8 }, { -7, 8 }, { -6, 8 }, { -5, 8 }, { -4, 8 }, { -3, 8 }, { -2, 8 }, { -1, 8 } }; + internal static readonly int[] VersionCodeArray = [31892, 34236, 39577, 42195, 48118, 51042, 55367, 58893, 63784, 68472, 70749, 76311, 79154, 84390, 87683, 92361, 96236, 102084, 102881, 110507, 110734, 117786, 119615, 126325, 127568, 133589, 136944, 141498, 145311, 150283, 152622, 158308, 161089, 167017]; + internal const byte White = 0; + internal const byte Black = 1; + internal const byte NonData = 2; + internal const byte Fixed = 4; + internal const byte DataWhite = 0; + internal const byte DataBlack = 1; + internal const byte FormatWhite = 2; + internal const byte FormatBlack = 3; + internal const byte FixedWhite = 6; + internal const byte FixedBlack = 7; + internal static readonly byte[,] FinderPatternTopLeft = new byte[9, 9] { { 7, 7, 7, 7, 7, 7, 7, 6, 2 }, { 7, 6, 6, 6, 6, 6, 7, 6, 2 }, { 7, 6, 7, 7, 7, 6, 7, 6, 2 }, { 7, 6, 7, 7, 7, 6, 7, 6, 2 }, { 7, 6, 7, 7, 7, 6, 7, 6, 2 }, { 7, 6, 6, 6, 6, 6, 7, 6, 2 }, { 7, 7, 7, 7, 7, 7, 7, 6, 2 }, { 6, 6, 6, 6, 6, 6, 6, 6, 2 }, { 2, 2, 2, 2, 2, 2, 2, 2, 2 } }; + internal static readonly byte[,] FinderPatternTopRight = new byte[9, 8] { { 6, 7, 7, 7, 7, 7, 7, 7 }, { 6, 7, 6, 6, 6, 6, 6, 7 }, { 6, 7, 6, 7, 7, 7, 6, 7 }, { 6, 7, 6, 7, 7, 7, 6, 7 }, { 6, 7, 6, 7, 7, 7, 6, 7 }, { 6, 7, 6, 6, 6, 6, 6, 7 }, { 6, 7, 7, 7, 7, 7, 7, 7 }, { 6, 6, 6, 6, 6, 6, 6, 6 }, { 2, 2, 2, 2, 2, 2, 2, 2 } }; + internal static readonly byte[,] FinderPatternBottomLeft = new byte[8, 9] { { 6, 6, 6, 6, 6, 6, 6, 6, 7 }, { 7, 7, 7, 7, 7, 7, 7, 6, 2 }, { 7, 6, 6, 6, 6, 6, 7, 6, 2 }, { 7, 6, 7, 7, 7, 6, 7, 6, 2 }, { 7, 6, 7, 7, 7, 6, 7, 6, 2 }, { 7, 6, 7, 7, 7, 6, 7, 6, 2 }, { 7, 6, 6, 6, 6, 6, 7, 6, 2 }, { 7, 7, 7, 7, 7, 7, 7, 6, 2 } }; + internal static readonly byte[,] AlignmentPattern = new byte[5, 5] { { 7, 7, 7, 7, 7 }, { 7, 6, 6, 6, 7 }, { 7, 6, 7, 6, 7 }, { 7, 6, 6, 6, 7 }, { 7, 7, 7, 7, 7 } }; + + public bool[,] QRCodeMatrix { get; private set; } = new bool[0, 0]; + + public int QRCodeVersion { get; private set; } + + public int QRCodeDimension { get; private set; } + + public ErrorCorrection ErrorCorrection + { + get; + set + { + field = value is >= ErrorCorrection.L and <= ErrorCorrection.H ? value : throw new ArgumentException("Error correction is invalid. Must be L, M, Q or H. Default is M"); + } + } = ErrorCorrection.H; + + public int ECIAssignValue + { + get; + set + { + field = value is >= -1 and <= 999999 ? value : throw new ArgumentException("ECI Assignment Value must be 0-999999 or -1 for none"); + } + } = -1; + + public static bool[,] Generate(string data, ErrorCorrection errorCorrection = ErrorCorrection.H) + { + return new QrCode(data, errorCorrection).QRCodeMatrix; + } + + public QrCode(string stringDataSegment, ErrorCorrection errorCorrection) + { + if (string.IsNullOrEmpty(stringDataSegment)) throw new ArgumentException("String data segment is null or missing"); + ErrorCorrection = errorCorrection; + _ = _encode([Encoding.UTF8.GetBytes(stringDataSegment)]); + } + + public QrCode(byte[] singleDataSeg, ErrorCorrection errorCorrection) + { + if (singleDataSeg is null || singleDataSeg.Length == 0) throw new ArgumentException("Single data segment argument is null or empty"); + ErrorCorrection = errorCorrection; + _ = _encode([singleDataSeg]); + } + + private bool[,] _encode(byte[][] DataSegArray) + { + if (DataSegArray == null || DataSegArray.Length == 0) + throw new ArgumentException("Data segments argument is null or empty"); + QRCodeVersion = 0; + QRCodeDimension = 0; + int num = 0; + for (int index = 0; index < DataSegArray.Length; ++index) + { + byte[] dataSeg = DataSegArray[index]; + if (dataSeg == null) + DataSegArray[index] = []; + else + num += dataSeg.Length; + } + if (num == 0) + throw new ArgumentException("There is no data to encode."); + _dataSegArray = DataSegArray; + _initialization(); + _encodeData(); + _calculateErrorCorrection(); + _interleaveBlocks(); + _buildBaseMatrix(); + _loadMatrixWithData(); + _selectBestMask(); + _addFormatInformation(); + QRCodeMatrix = new bool[QRCodeDimension, QRCodeDimension]; + for (int index1 = 0; index1 < QRCodeDimension; ++index1) + { + for (int index2 = 0; index2 < QRCodeDimension; ++index2) + { + if ((_resultMatrix[index1, index2] & 1) != 0) + QRCodeMatrix[index1, index2] = true; + } + } + return QRCodeMatrix; + } + + private void _initialization() + { + _encodingSegMode = new EncodingMode[_dataSegArray.Length]; + _encodedDataBits = 0; + if (ECIAssignValue >= 0) + _encodedDataBits = ECIAssignValue > sbyte.MaxValue ? (ECIAssignValue > 16383 /*0x3FFF*/ ? 28 : 20) : 12; + for (int index1 = 0; index1 < _dataSegArray.Length; ++index1) + { + byte[] dataSeg = _dataSegArray[index1]; + int length = dataSeg.Length; + EncodingMode encodingMode = EncodingMode.Numeric; + for (int index2 = 0; index2 < length; ++index2) + { + int num = EncodingTable[dataSeg[index2]]; + if (num >= 10) + { + if (num < 45) + { + encodingMode = EncodingMode.AlphaNumeric; + } + else + { + encodingMode = EncodingMode.Byte; + break; + } + } + } + int num1 = 4; + switch (encodingMode) + { + case EncodingMode.Numeric: + num1 += 10 * (length / 3); + if (length % 3 == 1) + { + num1 += 4; + break; + } + if (length % 3 == 2) + { + num1 += 7; + break; + } + break; + case EncodingMode.AlphaNumeric: + num1 += 11 * (length / 2); + if ((length & 1) != 0) + { + num1 += 6; + break; + } + break; + case EncodingMode.Byte: + num1 += 8 * length; + break; + } + _encodingSegMode[index1] = encodingMode; + _encodedDataBits += num1; + } + int num2 = 0; + for (QRCodeVersion = 1; QRCodeVersion <= 40; ++QRCodeVersion) + { + QRCodeDimension = 17 + (4 * QRCodeVersion); + _setDataCodewordsLength(); + num2 = 0; + for (int index = 0; index < _encodingSegMode.Length; ++index) + num2 += _dataLengthBits(_encodingSegMode[index]); + if (_encodedDataBits + num2 <= _maxDataBits) + break; + } + if (QRCodeVersion > 40) + throw new ApplicationException("Input data string is too long"); + _encodedDataBits += num2; + } + + private void _encodeData() + { + _codewordsArray = new byte[_maxCodewords]; + _codewordsPtr = 0; + _bitBuffer = 0U; + _bitBufferLen = 0; + if (ECIAssignValue >= 0) + { + _saveBitsToCodewordsArray(7, 4); + if (ECIAssignValue <= sbyte.MaxValue) + _saveBitsToCodewordsArray(ECIAssignValue, 8); + else if (ECIAssignValue <= 16383 /*0x3FFF*/) + { + _saveBitsToCodewordsArray((ECIAssignValue >> 8) | 128 /*0x80*/, 8); + _saveBitsToCodewordsArray(ECIAssignValue & byte.MaxValue, 8); + } + else + { + _saveBitsToCodewordsArray((ECIAssignValue >> 16) /*0x10*/ | 192 /*0xC0*/, 8); + _saveBitsToCodewordsArray((ECIAssignValue >> 8) & byte.MaxValue, 8); + _saveBitsToCodewordsArray(ECIAssignValue & byte.MaxValue, 8); + } + } + for (int index1 = 0; index1 < _dataSegArray.Length; ++index1) + { + byte[] dataSeg = _dataSegArray[index1]; + int length = dataSeg.Length; + _saveBitsToCodewordsArray((int)_encodingSegMode[index1], 4); + _saveBitsToCodewordsArray(length, _dataLengthBits(_encodingSegMode[index1])); + switch (_encodingSegMode[index1]) + { + case EncodingMode.Numeric: + int index2 = length / 3 * 3; + for (int index3 = 0; index3 < index2; index3 += 3) + _saveBitsToCodewordsArray((100 * EncodingTable[dataSeg[index3]]) + (10 * EncodingTable[dataSeg[index3 + 1]]) + EncodingTable[dataSeg[index3 + 2]], 10); + if (length - index2 == 1) + { + _saveBitsToCodewordsArray(EncodingTable[dataSeg[index2]], 4); + break; + } + if (length - index2 == 2) + { + _saveBitsToCodewordsArray((10 * EncodingTable[dataSeg[index2]]) + EncodingTable[dataSeg[index2 + 1]], 7); + break; + } + break; + case EncodingMode.AlphaNumeric: + int index4 = length / 2 * 2; + for (int index5 = 0; index5 < index4; index5 += 2) + _saveBitsToCodewordsArray((45 * EncodingTable[dataSeg[index5]]) + EncodingTable[dataSeg[index5 + 1]], 11); + if (length - index4 == 1) + { + _saveBitsToCodewordsArray(EncodingTable[dataSeg[index4]], 6); + break; + } + break; + case EncodingMode.Byte: + for (int index6 = 0; index6 < length; ++index6) + _saveBitsToCodewordsArray(dataSeg[index6], 8); + break; + } + } + if (_encodedDataBits < _maxDataBits) + _saveBitsToCodewordsArray(0, _maxDataBits - _encodedDataBits < 4 ? _maxDataBits - _encodedDataBits : 4); + if (_bitBufferLen > 0) + _codewordsArray[_codewordsPtr++] = (byte)(_bitBuffer >> 24); + int num = _maxDataCodewords - _codewordsPtr; + for (int index = 0; index < num; ++index) + _codewordsArray[_codewordsPtr + index] = (index & 1) == 0 ? (byte)236 : (byte)17; + } + + private void _saveBitsToCodewordsArray(int Data, int Bits) + { + _bitBuffer |= (uint)(Data << (32 /*0x20*/ - _bitBufferLen - Bits)); + for (_bitBufferLen += Bits; _bitBufferLen >= 8; _bitBufferLen -= 8) + { + _codewordsArray[_codewordsPtr++] = (byte)(_bitBuffer >> 24); + _bitBuffer <<= 8; + } + } + + private void _calculateErrorCorrection() + { + byte[] gen = GenArray[_errCorrCodewords - 7] ?? []; + byte[] numArray = new byte[Math.Max(_dataCodewordsGroup1, _dataCodewordsGroup2) + _errCorrCodewords]; + int num1 = _dataCodewordsGroup1; + int PolyLength = num1 + _errCorrCodewords; + int sourceIndex = 0; + int maxDataCodewords = _maxDataCodewords; + int num2 = _blocksGroup1 + _blocksGroup2; + for (int index = 0; index < num2; ++index) + { + if (index == _blocksGroup1) + { + num1 = _dataCodewordsGroup2; + PolyLength = num1 + _errCorrCodewords; + } + Array.Copy(_codewordsArray, sourceIndex, numArray, 0, num1); + Array.Clear(numArray, num1, _errCorrCodewords); + sourceIndex += num1; + _polynominalDivision(numArray, PolyLength, gen, _errCorrCodewords); + Array.Copy(numArray, num1, _codewordsArray, maxDataCodewords, _errCorrCodewords); + maxDataCodewords += _errCorrCodewords; + } + } + + private static void _polynominalDivision( + byte[] Polynomial, + int PolyLength, + byte[] _generator, + int ErrCorrCodewords) + { + int num1 = PolyLength - ErrCorrCodewords; + for (int index1 = 0; index1 < num1; ++index1) + { + if (Polynomial[index1] != 0) + { + int num2 = IntToExp[Polynomial[index1]]; + for (int index2 = 0; index2 < ErrCorrCodewords; ++index2) + Polynomial[index1 + 1 + index2] = (byte)(Polynomial[index1 + 1 + index2] ^ (uint)ExpToInt[_generator[index2] + num2]); + } + } + } + + private void _interleaveBlocks() + { + byte[] numArray1 = new byte[_maxCodewords]; + int length = _blocksGroup1 + _blocksGroup2; + int[] numArray2 = new int[length]; + for (int index = 1; index < length; ++index) + numArray2[index] = numArray2[index - 1] + (index <= _blocksGroup1 ? _dataCodewordsGroup1 : _dataCodewordsGroup2); + int num = _dataCodewordsGroup1 * length; + int index1 = 0; + int index2; + for (index2 = 0; index2 < num; ++index2) + { + numArray1[index2] = _codewordsArray[numArray2[index1]]; + ++numArray2[index1]; + ++index1; + if (index1 == length) + index1 = 0; + } + if (_dataCodewordsGroup2 > _dataCodewordsGroup1) + { + int maxDataCodewords = _maxDataCodewords; + int blocksGroup1 = _blocksGroup1; + for (; index2 < maxDataCodewords; ++index2) + { + numArray1[index2] = _codewordsArray[numArray2[blocksGroup1]]; + ++numArray2[blocksGroup1]; + ++blocksGroup1; + if (blocksGroup1 == length) + blocksGroup1 = _blocksGroup1; + } + } + numArray2[0] = _maxDataCodewords; + for (int index3 = 1; index3 < length; ++index3) + numArray2[index3] = numArray2[index3 - 1] + _errCorrCodewords; + int maxCodewords = _maxCodewords; + int index4 = 0; + for (; index2 < maxCodewords; ++index2) + { + numArray1[index2] = _codewordsArray[numArray2[index4]]; + ++numArray2[index4]; + ++index4; + if (index4 == length) + index4 = 0; + } + _codewordsArray = numArray1; + } + + private void _loadMatrixWithData() + { + int num1 = 0; + int num2 = 8 * _maxCodewords; + int index1 = QRCodeDimension - 1; + int index2 = QRCodeDimension - 1; + int num3 = 0; + while (true) + { + if ((_baseMatrix[index1, index2] & 2) == 0) + { + if ((_codewordsArray[num1 >> 3] & (1 << (7 - (num1 & 7)))) != 0) + _baseMatrix[index1, index2] = 1; + if (++num1 == num2) + break; + } + else if (index2 == 6) + --index2; + switch (num3) + { + case 0: + --index2; + num3 = 1; + continue; + case 1: + ++index2; + --index1; + if (index1 >= 0) + { + num3 = 0; + continue; + } + index2 -= 2; + index1 = 0; + num3 = 2; + continue; + case 2: + --index2; + num3 = 3; + continue; + case 3: + ++index2; + ++index1; + if (index1 < QRCodeDimension) + { + num3 = 2; + continue; + } + index2 -= 2; + index1 = QRCodeDimension - 1; + num3 = 0; + continue; + default: + continue; + } + } + } + + private void _selectBestMask() + { + int num1 = int.MaxValue; + _maskCode = 0; + for (int Mask = 0; Mask < 8; ++Mask) + { + _applyMask(Mask); + int num2 = _evaluationCondition1(); + if (num2 < num1) + { + int num3 = num2 + _evaluationCondition2(); + if (num3 < num1) + { + int num4 = num3 + _evaluationCondition3(); + if (num4 < num1) + { + int num5 = num4 + _evaluationCondition4(); + if (num5 < num1) + { + _resultMatrix = _maskMatrix; + _maskMatrix = new byte[0, 0]; + num1 = num5; + _maskCode = Mask; + } + } + } + } + } + } + + private int _evaluationCondition1() + { + int num1 = 0; + for (int index1 = 0; index1 < QRCodeDimension; ++index1) + { + int num2 = 1; + for (int index2 = 1; index2 < QRCodeDimension; ++index2) + { + if (((_maskMatrix[index1, index2 - 1] ^ _maskMatrix[index1, index2]) & 1) != 0) + { + if (num2 >= 5) + num1 += num2 - 2; + num2 = 0; + } + ++num2; + } + if (num2 >= 5) + num1 += num2 - 2; + } + for (int index3 = 0; index3 < QRCodeDimension; ++index3) + { + int num3 = 1; + for (int index4 = 1; index4 < QRCodeDimension; ++index4) + { + if (((_maskMatrix[index4 - 1, index3] ^ _maskMatrix[index4, index3]) & 1) != 0) + { + if (num3 >= 5) + num1 += num3 - 2; + num3 = 0; + } + ++num3; + } + if (num3 >= 5) + num1 += num3 - 2; + } + return num1; + } + + private int _evaluationCondition2() + { + int num = 0; + for (int index1 = 1; index1 < QRCodeDimension; ++index1) + { + for (int index2 = 1; index2 < QRCodeDimension; ++index2) + { + if ((_maskMatrix[index1 - 1, index2 - 1] & _maskMatrix[index1 - 1, index2] & _maskMatrix[index1, index2 - 1] & _maskMatrix[index1, index2] & 1) != 0) + num += 3; + else if (((_maskMatrix[index1 - 1, index2 - 1] | _maskMatrix[index1 - 1, index2] | _maskMatrix[index1, index2 - 1] | _maskMatrix[index1, index2]) & 1) == 0) + num += 3; + } + } + return num; + } + + private int _evaluationCondition3() + { + int num1 = 0; + for (int Row = 0; Row < QRCodeDimension; ++Row) + { + int num2 = 0; + for (int Col = 0; Col < QRCodeDimension; ++Col) + { + if ((_maskMatrix[Row, Col] & 1) != 0) + { + if (Col - num2 >= 4) + { + if (num2 >= 7 && _testHorizontalDarkLight(Row, num2 - 7)) + num1 += 40; + if (QRCodeDimension - Col >= 7 && _testHorizontalDarkLight(Row, Col)) + { + num1 += 40; + Col += 6; + } + } + num2 = Col + 1; + } + } + if (QRCodeDimension - num2 >= 4 && num2 >= 7 && _testHorizontalDarkLight(Row, num2 - 7)) + num1 += 40; + } + for (int Col = 0; Col < QRCodeDimension; ++Col) + { + int num3 = 0; + for (int Row = 0; Row < QRCodeDimension; ++Row) + { + if ((_maskMatrix[Row, Col] & 1) != 0) + { + if (Row - num3 >= 4) + { + if (num3 >= 7 && _testVerticalDarkLight(num3 - 7, Col)) + num1 += 40; + if (QRCodeDimension - Row >= 7 && _testVerticalDarkLight(Row, Col)) + { + num1 += 40; + Row += 6; + } + } + num3 = Row + 1; + } + } + if (QRCodeDimension - num3 >= 4 && num3 >= 7 && _testVerticalDarkLight(num3 - 7, Col)) + num1 += 40; + } + return num1; + } + + private int _evaluationCondition4() + { + int num1 = 0; + for (int index1 = 0; index1 < QRCodeDimension; ++index1) + { + for (int index2 = 0; index2 < QRCodeDimension; ++index2) + { + if ((_maskMatrix[index1, index2] & 1) != 0) + ++num1; + } + } + double num2 = num1 / (double)(QRCodeDimension * QRCodeDimension); + return num2 > 0.55 ? (int)(20.0 * (num2 - 0.5)) * 10 : num2 < 0.45 ? (int)(20.0 * (0.5 - num2)) * 10 : 0; + } + + private bool _testHorizontalDarkLight(int Row, int Col) + { + return (_maskMatrix[Row, Col] & ~_maskMatrix[Row, Col + 1] & _maskMatrix[Row, Col + 2] & _maskMatrix[Row, Col + 3] & _maskMatrix[Row, Col + 4] & ~_maskMatrix[Row, Col + 5] & _maskMatrix[Row, Col + 6] & 1) != 0; + } + + private bool _testVerticalDarkLight(int Row, int Col) + { + return (_maskMatrix[Row, Col] & ~_maskMatrix[Row + 1, Col] & _maskMatrix[Row + 2, Col] & _maskMatrix[Row + 3, Col] & _maskMatrix[Row + 4, Col] & ~_maskMatrix[Row + 5, Col] & _maskMatrix[Row + 6, Col] & 1) != 0; + } + + private void _addFormatInformation() + { + if (QRCodeVersion >= 7) + { + int num1 = QRCodeDimension - 11; + int versionCode = VersionCodeArray[QRCodeVersion - 7]; + int num2 = 1; + for (int index1 = 0; index1 < 6; ++index1) + { + for (int index2 = 0; index2 < 3; ++index2) + { + _resultMatrix[index1, num1 + index2] = (versionCode & num2) != 0 ? (byte)7 : (byte)6; + num2 <<= 1; + } + } + int num3 = 1; + for (int index3 = 0; index3 < 6; ++index3) + { + for (int index4 = 0; index4 < 3; ++index4) + { + _resultMatrix[num1 + index4, index3] = (versionCode & num3) != 0 ? (byte)7 : (byte)6; + num3 <<= 1; + } + } + } + int num4 = 0; + switch (ErrorCorrection) + { + case ErrorCorrection.L: + num4 = 8; + break; + case ErrorCorrection.Q: + num4 = 24; + break; + case ErrorCorrection.H: + num4 = 16 /*0x10*/; + break; + } + int formatInfo = FormatInfoArray[num4 + _maskCode]; + int num5 = 1; + for (int index5 = 0; index5 < 15; ++index5) + { + int num6 = (formatInfo & num5) != 0 ? 7 : 6; + num5 <<= 1; + _resultMatrix[FormatInfoOne[index5, 0], FormatInfoOne[index5, 1]] = (byte)num6; + int index6 = FormatInfoTwo[index5, 0]; + if (index6 < 0) + index6 += QRCodeDimension; + int index7 = FormatInfoTwo[index5, 1]; + if (index7 < 0) + index7 += QRCodeDimension; + _resultMatrix[index6, index7] = (byte)num6; + } + } + + private int _dataLengthBits(EncodingMode EncodingMode) + { + switch (EncodingMode) + { + case EncodingMode.Numeric: + if (QRCodeVersion < 10) + return 10; + return QRCodeVersion >= 27 ? 14 : 12; + case EncodingMode.AlphaNumeric: + if (QRCodeVersion < 10) + return 9; + return QRCodeVersion >= 27 ? 13 : 11; + case EncodingMode.Byte: + return QRCodeVersion >= 10 ? 16 /*0x10*/ : 8; + default: + throw new ApplicationException("Encoding mode error"); + } + } + + private void _setDataCodewordsLength() + { + int index = (int)(((QRCodeVersion - 1) * 4) + ErrorCorrection); + _blocksGroup1 = ECBlockInfo[index, 0]; + _dataCodewordsGroup1 = ECBlockInfo[index, 1]; + _blocksGroup2 = ECBlockInfo[index, 2]; + _dataCodewordsGroup2 = ECBlockInfo[index, 3]; + _maxDataCodewords = (_blocksGroup1 * _dataCodewordsGroup1) + (_blocksGroup2 * _dataCodewordsGroup2); + _maxDataBits = 8 * _maxDataCodewords; + _maxCodewords = MaxCodewordsArray[QRCodeVersion]; + _errCorrCodewords = (_maxCodewords - _maxDataCodewords) / (_blocksGroup1 + _blocksGroup2); + } + + private void _buildBaseMatrix() + { + _baseMatrix = new byte[QRCodeDimension + 5, QRCodeDimension + 5]; + for (int index1 = 0; index1 < 9; ++index1) + { + for (int index2 = 0; index2 < 9; ++index2) + _baseMatrix[index1, index2] = FinderPatternTopLeft[index1, index2]; + } + int num1 = QRCodeDimension - 8; + for (int index3 = 0; index3 < 9; ++index3) + { + for (int index4 = 0; index4 < 8; ++index4) + _baseMatrix[index3, num1 + index4] = FinderPatternTopRight[index3, index4]; + } + for (int index5 = 0; index5 < 8; ++index5) + { + for (int index6 = 0; index6 < 9; ++index6) + _baseMatrix[num1 + index5, index6] = FinderPatternBottomLeft[index5, index6]; + } + for (int index = 8; index < QRCodeDimension - 8; ++index) + _baseMatrix[index, 6] = _baseMatrix[6, index] = (index & 1) == 0 ? (byte)7 : (byte)6; + if (QRCodeVersion > 1) + { + byte[] alignmentPosition = AlignmentPositionArray[QRCodeVersion] ?? []; + int length = alignmentPosition.Length; + for (int index7 = 0; index7 < length; ++index7) + { + for (int index8 = 0; index8 < length; ++index8) + { + if ((index8 != 0 || index7 != 0) && (index8 != length - 1 || index7 != 0) && (index8 != 0 || index7 != length - 1)) + { + int num2 = alignmentPosition[index7]; + int num3 = alignmentPosition[index8]; + for (int index9 = -2; index9 < 3; ++index9) + { + for (int index10 = -2; index10 < 3; ++index10) + _baseMatrix[num2 + index9, num3 + index10] = AlignmentPattern[index9 + 2, index10 + 2]; + } + } + } + } + } + if (QRCodeVersion < 7) + return; + int num4 = QRCodeDimension - 11; + for (int index11 = 0; index11 < 6; ++index11) + { + for (int index12 = 0; index12 < 3; ++index12) + _baseMatrix[index11, num4 + index12] = 2; + } + for (int index13 = 0; index13 < 6; ++index13) + { + for (int index14 = 0; index14 < 3; ++index14) + _baseMatrix[num4 + index14, index13] = 2; + } + } + + private void _applyMask(int Mask) + { + _maskMatrix = (byte[,])_baseMatrix.Clone(); + switch (Mask) + { + case 0: + _applyMask0(); + break; + case 1: + _applyMask1(); + break; + case 2: + _applyMask2(); + break; + case 3: + _applyMask3(); + break; + case 4: + _applyMask4(); + break; + case 5: + _applyMask5(); + break; + case 6: + _applyMask6(); + break; + case 7: + _applyMask7(); + break; + } + } + + private void _applyMask0() + { + for (int index1 = 0; index1 < QRCodeDimension; index1 += 2) + { + for (int index2 = 0; index2 < QRCodeDimension; index2 += 2) + { + if ((_maskMatrix[index1, index2] & 2) == 0) + _maskMatrix[index1, index2] ^= 1; + if ((_maskMatrix[index1 + 1, index2 + 1] & 2) == 0) + _maskMatrix[index1 + 1, index2 + 1] ^= 1; + } + } + } + + private void _applyMask1() + { + for (int index1 = 0; index1 < QRCodeDimension; index1 += 2) + { + for (int index2 = 0; index2 < QRCodeDimension; ++index2) + { + if ((_maskMatrix[index1, index2] & 2) == 0) + _maskMatrix[index1, index2] ^= 1; + } + } + } + + private void _applyMask2() + { + for (int index1 = 0; index1 < QRCodeDimension; ++index1) + { + for (int index2 = 0; index2 < QRCodeDimension; index2 += 3) + { + if ((_maskMatrix[index1, index2] & 2) == 0) + _maskMatrix[index1, index2] ^= 1; + } + } + } + + private void _applyMask3() + { + for (int index1 = 0; index1 < QRCodeDimension; index1 += 3) + { + for (int index2 = 0; index2 < QRCodeDimension; index2 += 3) + { + if ((_maskMatrix[index1, index2] & 2) == 0) + _maskMatrix[index1, index2] ^= 1; + if ((_maskMatrix[index1 + 1, index2 + 2] & 2) == 0) + _maskMatrix[index1 + 1, index2 + 2] ^= 1; + if ((_maskMatrix[index1 + 2, index2 + 1] & 2) == 0) + _maskMatrix[index1 + 2, index2 + 1] ^= 1; + } + } + } + + private void _applyMask4() + { + for (int index1 = 0; index1 < QRCodeDimension; index1 += 4) + { + for (int index2 = 0; index2 < QRCodeDimension; index2 += 6) + { + if ((_maskMatrix[index1, index2] & 2) == 0) + _maskMatrix[index1, index2] ^= 1; + if ((_maskMatrix[index1, index2 + 1] & 2) == 0) + _maskMatrix[index1, index2 + 1] ^= 1; + if ((_maskMatrix[index1, index2 + 2] & 2) == 0) + _maskMatrix[index1, index2 + 2] ^= 1; + if ((_maskMatrix[index1 + 1, index2] & 2) == 0) + _maskMatrix[index1 + 1, index2] ^= 1; + if ((_maskMatrix[index1 + 1, index2 + 1] & 2) == 0) + _maskMatrix[index1 + 1, index2 + 1] ^= 1; + if ((_maskMatrix[index1 + 1, index2 + 2] & 2) == 0) + _maskMatrix[index1 + 1, index2 + 2] ^= 1; + if ((_maskMatrix[index1 + 2, index2 + 3] & 2) == 0) + _maskMatrix[index1 + 2, index2 + 3] ^= 1; + if ((_maskMatrix[index1 + 2, index2 + 4] & 2) == 0) + _maskMatrix[index1 + 2, index2 + 4] ^= 1; + if ((_maskMatrix[index1 + 2, index2 + 5] & 2) == 0) + _maskMatrix[index1 + 2, index2 + 5] ^= 1; + if ((_maskMatrix[index1 + 3, index2 + 3] & 2) == 0) + _maskMatrix[index1 + 3, index2 + 3] ^= 1; + if ((_maskMatrix[index1 + 3, index2 + 4] & 2) == 0) + _maskMatrix[index1 + 3, index2 + 4] ^= 1; + if ((_maskMatrix[index1 + 3, index2 + 5] & 2) == 0) + _maskMatrix[index1 + 3, index2 + 5] ^= 1; + } + } + } + + private void _applyMask5() + { + for (int index1 = 0; index1 < QRCodeDimension; index1 += 6) + { + for (int index2 = 0; index2 < QRCodeDimension; index2 += 6) + { + for (int index3 = 0; index3 < 6; ++index3) + { + if ((_maskMatrix[index1, index2 + index3] & 2) == 0) + _maskMatrix[index1, index2 + index3] ^= 1; + } + for (int index4 = 1; index4 < 6; ++index4) + { + if ((_maskMatrix[index1 + index4, index2] & 2) == 0) + _maskMatrix[index1 + index4, index2] ^= 1; + } + if ((_maskMatrix[index1 + 2, index2 + 3] & 2) == 0) + _maskMatrix[index1 + 2, index2 + 3] ^= 1; + if ((_maskMatrix[index1 + 3, index2 + 2] & 2) == 0) + _maskMatrix[index1 + 3, index2 + 2] ^= 1; + if ((_maskMatrix[index1 + 3, index2 + 4] & 2) == 0) + _maskMatrix[index1 + 3, index2 + 4] ^= 1; + if ((_maskMatrix[index1 + 4, index2 + 3] & 2) == 0) + _maskMatrix[index1 + 4, index2 + 3] ^= 1; + } + } + } + + private void _applyMask6() + { + for (int index1 = 0; index1 < QRCodeDimension; index1 += 6) + { + for (int index2 = 0; index2 < QRCodeDimension; index2 += 6) + { + for (int index3 = 0; index3 < 6; ++index3) + { + if ((_maskMatrix[index1, index2 + index3] & 2) == 0) + _maskMatrix[index1, index2 + index3] ^= 1; + } + for (int index4 = 1; index4 < 6; ++index4) + { + if ((_maskMatrix[index1 + index4, index2] & 2) == 0) + _maskMatrix[index1 + index4, index2] ^= 1; + } + if ((_maskMatrix[index1 + 1, index2 + 1] & 2) == 0) + _maskMatrix[index1 + 1, index2 + 1] ^= 1; + if ((_maskMatrix[index1 + 1, index2 + 2] & 2) == 0) + _maskMatrix[index1 + 1, index2 + 2] ^= 1; + if ((_maskMatrix[index1 + 2, index2 + 1] & 2) == 0) + _maskMatrix[index1 + 2, index2 + 1] ^= 1; + if ((_maskMatrix[index1 + 2, index2 + 3] & 2) == 0) + _maskMatrix[index1 + 2, index2 + 3] ^= 1; + if ((_maskMatrix[index1 + 2, index2 + 4] & 2) == 0) + _maskMatrix[index1 + 2, index2 + 4] ^= 1; + if ((_maskMatrix[index1 + 3, index2 + 2] & 2) == 0) + _maskMatrix[index1 + 3, index2 + 2] ^= 1; + if ((_maskMatrix[index1 + 3, index2 + 4] & 2) == 0) + _maskMatrix[index1 + 3, index2 + 4] ^= 1; + if ((_maskMatrix[index1 + 4, index2 + 2] & 2) == 0) + _maskMatrix[index1 + 4, index2 + 2] ^= 1; + if ((_maskMatrix[index1 + 4, index2 + 3] & 2) == 0) + _maskMatrix[index1 + 4, index2 + 3] ^= 1; + if ((_maskMatrix[index1 + 4, index2 + 5] & 2) == 0) + _maskMatrix[index1 + 4, index2 + 5] ^= 1; + if ((_maskMatrix[index1 + 5, index2 + 4] & 2) == 0) + _maskMatrix[index1 + 5, index2 + 4] ^= 1; + if ((_maskMatrix[index1 + 5, index2 + 5] & 2) == 0) + _maskMatrix[index1 + 5, index2 + 5] ^= 1; + } + } + } + + private void _applyMask7() + { + for (int index1 = 0; index1 < QRCodeDimension; index1 += 6) + { + for (int index2 = 0; index2 < QRCodeDimension; index2 += 6) + { + if ((_maskMatrix[index1, index2] & 2) == 0) + _maskMatrix[index1, index2] ^= 1; + if ((_maskMatrix[index1, index2 + 2] & 2) == 0) + _maskMatrix[index1, index2 + 2] ^= 1; + if ((_maskMatrix[index1, index2 + 4] & 2) == 0) + _maskMatrix[index1, index2 + 4] ^= 1; + if ((_maskMatrix[index1 + 1, index2 + 3] & 2) == 0) + _maskMatrix[index1 + 1, index2 + 3] ^= 1; + if ((_maskMatrix[index1 + 1, index2 + 4] & 2) == 0) + _maskMatrix[index1 + 1, index2 + 4] ^= 1; + if ((_maskMatrix[index1 + 1, index2 + 5] & 2) == 0) + _maskMatrix[index1 + 1, index2 + 5] ^= 1; + if ((_maskMatrix[index1 + 2, index2] & 2) == 0) + _maskMatrix[index1 + 2, index2] ^= 1; + if ((_maskMatrix[index1 + 2, index2 + 4] & 2) == 0) + _maskMatrix[index1 + 2, index2 + 4] ^= 1; + if ((_maskMatrix[index1 + 2, index2 + 5] & 2) == 0) + _maskMatrix[index1 + 2, index2 + 5] ^= 1; + if ((_maskMatrix[index1 + 3, index2 + 1] & 2) == 0) + _maskMatrix[index1 + 3, index2 + 1] ^= 1; + if ((_maskMatrix[index1 + 3, index2 + 3] & 2) == 0) + _maskMatrix[index1 + 3, index2 + 3] ^= 1; + if ((_maskMatrix[index1 + 3, index2 + 5] & 2) == 0) + _maskMatrix[index1 + 3, index2 + 5] ^= 1; + if ((_maskMatrix[index1 + 4, index2] & 2) == 0) + _maskMatrix[index1 + 4, index2] ^= 1; + if ((_maskMatrix[index1 + 4, index2 + 1] & 2) == 0) + _maskMatrix[index1 + 4, index2 + 1] ^= 1; + if ((_maskMatrix[index1 + 4, index2 + 2] & 2) == 0) + _maskMatrix[index1 + 4, index2 + 2] ^= 1; + if ((_maskMatrix[index1 + 5, index2 + 1] & 2) == 0) + _maskMatrix[index1 + 5, index2 + 1] ^= 1; + if ((_maskMatrix[index1 + 5, index2 + 2] & 2) == 0) + _maskMatrix[index1 + 5, index2 + 2] ^= 1; + if ((_maskMatrix[index1 + 5, index2 + 3] & 2) == 0) + _maskMatrix[index1 + 5, index2 + 3] ^= 1; + } + } + } +} diff --git a/Core/Utils/StaticMethods.cs b/Core/Utils/StaticMethods.cs new file mode 100644 index 0000000..f478ef5 --- /dev/null +++ b/Core/Utils/StaticMethods.cs @@ -0,0 +1,26 @@ +using System.Text.RegularExpressions; +using Upsilon.Apps.Passkey.Interfaces.Utils; + +namespace Upsilon.Apps.Passkey.Core.Utils +{ + public static class StaticMethods + { + public static string ToSentenceCase(this string str) => Regex.Replace(str, "[a-z][A-Z]", m => $"{m.Value[0]} {char.ToLower(m.Value[1])}"); + + public static string SerializeWith(this T obj, ISerializationCenter serializationCenter) where T : notnull + => serializationCenter.Serialize(obj); + + public static T DeserializeTo(this string serializedString, ISerializationCenter serializationCenter) where T : notnull + => serializationCenter.Deserialize(serializedString); + + public static T CloneWith(this T source, ISerializationCenter serializationCenter) where T : notnull + { + return source.SerializeWith(serializationCenter).DeserializeTo(serializationCenter); + } + + public static bool AreDifferent(this ISerializationCenter serializationCenter, object object1, object object2) + { + return object1.SerializeWith(serializationCenter) != object2.SerializeWith(serializationCenter); + } + } +} diff --git a/GUI/WPF/App.xaml b/GUI/WPF/App.xaml new file mode 100644 index 0000000..d10e95e --- /dev/null +++ b/GUI/WPF/App.xaml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/GUI/WPF/App.xaml.cs b/GUI/WPF/App.xaml.cs new file mode 100644 index 0000000..4263559 --- /dev/null +++ b/GUI/WPF/App.xaml.cs @@ -0,0 +1,12 @@ +using System.Windows; + +namespace Upsilon.Apps.Passkey.GUI.WPF +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } + +} diff --git a/GUI/WPF/AssemblyInfo.cs b/GUI/WPF/AssemblyInfo.cs new file mode 100644 index 0000000..372e037 --- /dev/null +++ b/GUI/WPF/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/GUI/WPF/Helper/EnumHelper.cs b/GUI/WPF/Helper/EnumHelper.cs new file mode 100644 index 0000000..b59a10c --- /dev/null +++ b/GUI/WPF/Helper/EnumHelper.cs @@ -0,0 +1,74 @@ +using Upsilon.Apps.Passkey.Core.Utils; +using Upsilon.Apps.Passkey.Interfaces.Enums; + +namespace Upsilon.Apps.Passkey.GUI.WPF.Helper +{ + internal static class EnumHelper + { + public static string ToReadableString(this ActivityEventType eventType) + { + return eventType switch + { + ActivityEventType.None => "All", + ActivityEventType.MergeAndSaveThenRemoveAutoSaveFile => "Auto-save merged then saved", + ActivityEventType.MergeWithoutSavingAndKeepAutoSaveFile => "Auto-save merged but not saved", + ActivityEventType.DontMergeAndRemoveAutoSaveFile => "Auto-save discarded", + ActivityEventType.DontMergeAndKeepAutoSaveFile => "Auto-save not merged and keeped", + ActivityEventType.DatabaseCreated + or ActivityEventType.DatabaseOpened + or ActivityEventType.DatabaseSaved + or ActivityEventType.DatabaseClosed + or ActivityEventType.LoginSessionTimeoutReached + or ActivityEventType.LoginFailed + or ActivityEventType.UserLoggedIn + or ActivityEventType.UserLoggedOut + or ActivityEventType.ImportingDataStarted + or ActivityEventType.ImportingDataSucceded + or ActivityEventType.ImportingDataFailed + or ActivityEventType.ExportingDataStarted + or ActivityEventType.ExportingDataSucceded + or ActivityEventType.ExportingDataFailed + or ActivityEventType.ItemUpdated + or ActivityEventType.ItemAdded + or ActivityEventType.ItemDeleted => eventType.ToString().ToSentenceCase(), + _ => throw new InvalidOperationException($"'{eventType}' event type not handled"), + }; + } + + public static ActivityEventType ActivityEventTypeFromReadableString(string readableString) + { + try + { + return Enum.GetValues() + .Cast() + .First(x => x.ToReadableString() == readableString); + } + catch + { + throw new InvalidOperationException($"'{readableString}' event type not handled"); + } + } + + public static string ToReadableString(this WarningType warningType) + { + return warningType switch + { + WarningType.PasswordUpdateReminderWarning | WarningType.PasswordLeakedWarning => "All", + WarningType.PasswordUpdateReminderWarning => "Expired passwords", + WarningType.PasswordLeakedWarning => "Leaked passwords", + _ => throw new InvalidOperationException($"'{warningType}' warning type not handled"), + }; + } + + public static WarningType ActivityWarningTypeFromReadableString(string readableString) + { + return readableString switch + { + "All" => WarningType.PasswordUpdateReminderWarning | WarningType.PasswordLeakedWarning, + "Expired passwords" => WarningType.PasswordUpdateReminderWarning, + "Leaked passwords" => WarningType.PasswordLeakedWarning, + _ => throw new InvalidOperationException($"'{readableString}' warning type not handled"), + }; + } + } +} diff --git a/GUI/WPF/Helper/HotKeyHelper.cs b/GUI/WPF/Helper/HotKeyHelper.cs new file mode 100644 index 0000000..04282a5 --- /dev/null +++ b/GUI/WPF/Helper/HotKeyHelper.cs @@ -0,0 +1,92 @@ +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Input; +using System.Windows.Interop; + +namespace Upsilon.Apps.Passkey.GUI.WPF.Helper +{ + public static class HotkeyHelper + { + private static int _id = 0; + + public static event EventHandler? HotkeyPressed; + + public static int Register(Window window, ModifierKeys modifiers, Key key) + { + int hotkeyId = Interlocked.Increment(ref _id); + uint virtualKey = (uint)KeyInterop.VirtualKeyFromKey(key); + + IntPtr hWnd = new WindowInteropHelper(window).Handle; + if (hWnd == IntPtr.Zero) + return -1; + + bool success = RegisterHotKey(hWnd, hotkeyId, (uint)modifiers, virtualKey); + if (!success) + return -1; + + if (PresentationSource.FromVisual(window) is HwndSource source) + { + source.AddHook((hwnd, msg, wParam, lParam, ref handled) => + { + if (msg == 0x0312 && (int)wParam == hotkeyId) + { + HotkeyEventArgs e = new(lParam); + HotkeyPressed?.Invoke(window, e); + handled = true; + } + return IntPtr.Zero; + }); + } + + return hotkeyId; + } + + public static bool Unregister(Window window, int hotkeyId) + { + if (window is null) + return false; + + IntPtr hWnd = new WindowInteropHelper(window).Handle; + return hWnd != nint.Zero && UnregisterHotKey(hWnd, hotkeyId); + } + + public static void Send(ModifierKeys modifiers, Key key) + { + //byte[] modifiers = []; + byte virtualKey = (byte)KeyInterop.VirtualKeyFromKey(key); + + keybd_event((byte)modifiers, 0, 0, UIntPtr.Zero); + + keybd_event(virtualKey, 0, 0, UIntPtr.Zero); + keybd_event(virtualKey, 0, 0x0002, UIntPtr.Zero); + + keybd_event((byte)modifiers, 0, 0x0002, UIntPtr.Zero); + } + + [DllImport("user32.dll")] + private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk); + + [DllImport("user32.dll")] + private static extern bool UnregisterHotKey(IntPtr hWnd, int id); + + [DllImport("user32.dll")] + private static extern uint MapVirtualKey(uint uCode, uint uMapType); + + [DllImport("user32.dll")] + private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo); + } + + public class HotkeyEventArgs : EventArgs + { + public readonly Key Key; + public readonly ModifierKeys Modifiers; + + internal HotkeyEventArgs(IntPtr hotKeyParam) + { + uint param = (uint)hotKeyParam.ToInt64(); + int virtualKey = (int)((param & 0xffff0000) >> 16); + Key = KeyInterop.KeyFromVirtualKey(virtualKey); + Modifiers = (ModifierKeys)(param & 0x0000ffff); + } + } +} \ No newline at end of file diff --git a/GUI/WPF/Helper/IItemHelper.cs b/GUI/WPF/Helper/IItemHelper.cs new file mode 100644 index 0000000..7001d68 --- /dev/null +++ b/GUI/WPF/Helper/IItemHelper.cs @@ -0,0 +1,57 @@ +using Upsilon.Apps.Passkey.Interfaces.Models; + +namespace Upsilon.Apps.Passkey.GUI.WPF.Helper +{ + public static class IItemHelper + { + public static void Shake(this IUser user) + { + _ = user.ItemId; + } + + public static bool MeetsFilterConditions(this IService service, string serviceFilter, string identifierFilter, string globalTextFilter, bool changedItemsOnly) + { + serviceFilter = serviceFilter.ToLower().Trim(); + identifierFilter = identifierFilter.ToLower().Trim(); + globalTextFilter = globalTextFilter.ToLower().Trim(); + + string serviceId = service.ItemId.Replace(service.User.ItemId, string.Empty).ToLower().Trim(); + string serviceName = service.ServiceName.ToLower().Trim(); + string url = service.Url.ToLower().Trim(); + string notes = service.Notes.ToLower().Trim(); + + return (!string.IsNullOrWhiteSpace(globalTextFilter) + ? serviceId == globalTextFilter + || serviceName.Contains(globalTextFilter) + || url.Contains(globalTextFilter) + || notes.Contains(globalTextFilter) + || service.Accounts.Any(x => x.MeetsFilterConditions(string.Empty, globalTextFilter, changedItemsOnly)) + : (string.IsNullOrWhiteSpace(serviceFilter) + || (!string.IsNullOrWhiteSpace(serviceFilter) && serviceName.Contains(serviceFilter))) + && (string.IsNullOrWhiteSpace(identifierFilter) + || service.Accounts.Any(x => x.MeetsFilterConditions(identifierFilter, globalTextFilter, changedItemsOnly)))) + && (!changedItemsOnly || service.HasChanged()); + } + + public static bool MeetsFilterConditions(this IAccount account, string identifierFilter, string globalTextFilter, bool changedItemsOnly) + { + identifierFilter = identifierFilter.ToLower().Trim(); + globalTextFilter = globalTextFilter.ToLower().Trim(); + + string accountId = account.ItemId.Replace(account.Service.ItemId, string.Empty).ToLower().Trim(); + string label = account.Label.ToLower().Trim(); + string notes = account.Notes.ToLower().Trim(); + string identifiers = string.Join("\n", account.Identifiers.Select(x => x.ToLower().Trim())); + + return (!string.IsNullOrWhiteSpace(globalTextFilter) + ? accountId == globalTextFilter + || identifiers.Contains(globalTextFilter) + || label.ToLower().Contains(globalTextFilter) + || notes.ToLower().Contains(globalTextFilter) + : string.IsNullOrWhiteSpace(identifierFilter) + || identifiers.Contains(identifierFilter) + || label.Contains(identifierFilter)) + && (!changedItemsOnly || account.HasChanged()); + } + } +} diff --git a/GUI/WPF/Helper/NumericTextBoxHelper.cs b/GUI/WPF/Helper/NumericTextBoxHelper.cs new file mode 100644 index 0000000..7b67942 --- /dev/null +++ b/GUI/WPF/Helper/NumericTextBoxHelper.cs @@ -0,0 +1,57 @@ +using System.Text.RegularExpressions; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace Upsilon.Apps.Passkey.GUI.WPF.Helper +{ + public static class NumericTextBoxHelper + { + private static readonly Regex _regex = new("[^0-9]+"); //regex that matches disallowed text + + private static bool _isTextAllowed(string text) + { + bool isValid = !_regex.IsMatch(text); + + if (isValid) + { + isValid = int.TryParse(text, out _); + } + + return isValid; + } + + public static void TextChanged(object sender, TextChangedEventArgs e) + { + TextBox textBox = (TextBox)sender; + + e.Handled = !_isTextAllowed(textBox.Text); + + if (e.Handled) + { + textBox.Text = textBox.Text.Replace(" ", ""); + } + } + + public static void PreviewTextInput(object sender, TextCompositionEventArgs e) + { + e.Handled = !_isTextAllowed(e.Text); + } + + public static void Pasting(object sender, DataObjectPastingEventArgs e) + { + if (e.DataObject.GetDataPresent(typeof(string))) + { + string text = (string)e.DataObject.GetData(typeof(string)); + if (!_isTextAllowed(text)) + { + e.CancelCommand(); + } + } + else + { + e.CancelCommand(); + } + } + } +} diff --git a/GUI/WPF/Helper/PropertyHelper.cs b/GUI/WPF/Helper/PropertyHelper.cs new file mode 100644 index 0000000..3f9625f --- /dev/null +++ b/GUI/WPF/Helper/PropertyHelper.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Upsilon.Apps.Passkey.GUI.WPF.Helper +{ + public static class PropertyHelper + { + public static bool SetProperty(ref T field, + T newValue, + INotifyPropertyChanged sender, + PropertyChangedEventHandler? PropertyChanged, + [CallerMemberName] string? propertyName = null) + { + if (!Equals(field, newValue)) + { + field = newValue; + PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs(propertyName)); + + return true; + } + + return false; + } + } +} diff --git a/GUI/WPF/Helper/WindowHelper.cs b/GUI/WPF/Helper/WindowHelper.cs new file mode 100644 index 0000000..3d17268 --- /dev/null +++ b/GUI/WPF/Helper/WindowHelper.cs @@ -0,0 +1,55 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using Upsilon.Apps.Passkey.GUI.WPF.Themes; + +namespace Upsilon.Apps.Passkey.GUI.WPF.Helper +{ + public static class WindowHelper + { + public static bool GetIsBusy(this Window window) + { + return window.Cursor == Cursors.Wait; + } + + public static void SetIsBusy(this Window window, bool isBusy) + { + window.Cursor = isBusy ? Cursors.Wait : Cursors.Arrow; + } + + public static bool GetIsBusy(this UserControl control) + { + return Window.GetWindow(control).GetIsBusy(); + } + + public static void PostLoadSetup(this Window window) + { + DarkMode.SetDarkMode(window); + ComputeTabIndex(window); + } + + public static void ComputeTabIndex(this Window window) + { + int tabIndex = 0; + _computeTabIndex(window, ref tabIndex); + } + + private static void _computeTabIndex(DependencyObject depObj, ref int tabIndex) + { + if (depObj == null) return; + + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++) + { + DependencyObject ithChild = VisualTreeHelper.GetChild(depObj, i); + + if (ithChild is Control control) + { + control.TabIndex = tabIndex++; + } + + _computeTabIndex(ithChild, ref tabIndex); + } + } + } +} diff --git a/GUI/WPF/MainWindow.xaml b/GUI/WPF/MainWindow.xaml new file mode 100644 index 0000000..439d99e --- /dev/null +++ b/GUI/WPF/MainWindow.xaml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GUI/WPF/MainWindow.xaml.cs b/GUI/WPF/MainWindow.xaml.cs new file mode 100644 index 0000000..1a0fa5b --- /dev/null +++ b/GUI/WPF/MainWindow.xaml.cs @@ -0,0 +1,215 @@ +using Microsoft.Win32; +using System.IO; +using System.Windows; +using System.Windows.Input; +using System.Windows.Threading; +using Upsilon.Apps.Passkey.Core.Models; +using Upsilon.Apps.Passkey.GUI.WPF.Helper; +using Upsilon.Apps.Passkey.GUI.WPF.ViewModels; +using Upsilon.Apps.Passkey.GUI.WPF.Views; + +namespace Upsilon.Apps.Passkey.GUI.WPF +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Window + { + private readonly MainViewModel _mainViewModel; + private readonly DispatcherTimer _timer; + + public MainWindow() + { + InitializeComponent(); + + DataContext = _mainViewModel = new MainViewModel(); + + _timer = new() + { + Interval = new TimeSpan(0, 0, 5), + }; + + _resetCredentials(); + + try + { + string[] args = Environment.GetCommandLineArgs(); + string databaseFile = Path.GetFullPath(Environment.GetCommandLineArgs()[1]); + if (File.Exists(databaseFile)) + { + _mainViewModel.DatabaseFile = databaseFile; + } + } + catch { } + + _username_TB.KeyUp += _credential_TB_KeyUp; + _password_PB.KeyUp += _credential_TB_KeyUp; + _timer.Tick += _timer_Elapsed; + Loaded += (s, e) => this.PostLoadSetup(); + } + + private void _timer_Elapsed(object? sender, EventArgs e) + { + _resetCredentials(); + MainViewModel.Database?.Close(); + MainViewModel.Database = null; + } + + private void _newUser_MenuItem_Click(object sender, RoutedEventArgs e) + { + UserSettingsView.ShowUserSettings(this); + } + + private void _generatePassword_MenuItem_Click(object sender, RoutedEventArgs e) + { + _ = PasswordGenerator.ShowGeneratePasswordDialog(this); + } + + private void _credential_TB_KeyUp(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + _timer.Stop(); + + if (sender == _username_TB) + { + if (string.IsNullOrEmpty(_username_TB.Text)) + { + _timer.Start(); + return; + } + + if (!File.Exists(_mainViewModel.DatabaseFile)) + { + string filename = MainViewModel.CryptographyCenter.GetHash(_username_TB.Text); + _mainViewModel.DatabaseFile = Path.GetFullPath($"{Path.GetDirectoryName(Environment.ProcessPath)}/raw/{filename}.pku"); + } + + try + { + MainViewModel.Database = Database.Open(MainViewModel.CryptographyCenter, + MainViewModel.SerializationCenter, + MainViewModel.PasswordFactory, + MainViewModel.ClipboardManager, + _mainViewModel.DatabaseFile, + _username_TB.Text); + + MainViewModel.Database.DatabaseClosed += _database_DatabaseClosed; + MainViewModel.Database.AutoSaveDetected += _database_AutoSaveDetected; + } + catch { } + + _mainViewModel.CredentialsLabel = "Password :"; + + _username_TB.Text = string.Empty; + _username_TB.Visibility = Visibility.Collapsed; + + _password_PB.Password = string.Empty; + _password_PB.Visibility = Visibility.Visible; + _ = _password_PB.Focus(); + } + else + { + if (string.IsNullOrEmpty(_password_PB.Password)) + { + _timer.Start(); + return; + } + + if (MainViewModel.Database is not null) + { + _ = MainViewModel.Database.Login(_password_PB.Password); + + if (MainViewModel.Database.User is not null) + { + Hide(); + _resetCredentials(); + + if (!UserServicesView.ShowUser(this)) + { + Close(); + } + else + { + _resetCredentials(); + } + } + } + } + + _password_PB.Password = string.Empty; + _timer.Start(); + } + else if (e.Key == Key.Escape) + { + _resetCredentials(); + MainViewModel.Database?.Close(); + MainViewModel.Database = null; + } + else + { + _timer.Stop(); + _timer.Start(); + } + } + + private void _database_AutoSaveDetected(object? sender, Interfaces.Events.AutoSaveDetectedEventArgs e) + { + Hide(); + + MessageBoxResult result = MessageBox.Show("Unsaved changes have been detected.\nClick Yes to apply these changes.\nClick No to discard them.\nClick Cancel to ignore and keep the save file.", "Autosave detected", MessageBoxButton.YesNoCancel, MessageBoxImage.Question); + + e.MergeBehavior = result switch + { + MessageBoxResult.Cancel => Passkey.Interfaces.Enums.AutoSaveMergeBehavior.MergeWithoutSavingAndKeepAutoSaveFile, + MessageBoxResult.No => Passkey.Interfaces.Enums.AutoSaveMergeBehavior.DontMergeAndRemoveAutoSaveFile, + _ => Passkey.Interfaces.Enums.AutoSaveMergeBehavior.MergeAndSaveThenRemoveAutoSaveFile, + }; + } + + private void _database_DatabaseClosed(object? sender, Interfaces.Events.LogoutEventArgs e) + { + try + { + Dispatcher.Invoke(() => + { + _resetCredentials(); + MainViewModel.Database = null; + Show(); + }); + } + catch { } + } + + private void _resetCredentials() + { + _mainViewModel.DatabaseFile = string.Empty; + _mainViewModel.CredentialsLabel = "Username :"; + + _username_TB.Text = string.Empty; + _username_TB.Visibility = Visibility.Visible; + _ = _username_TB.Focus(); + + _password_PB.Password = string.Empty; + _password_PB.Visibility = Visibility.Collapsed; + + _timer.Stop(); + } + + private void _openDatabase_MenuItem_Click(object sender, RoutedEventArgs e) + { + OpenFileDialog dialog = new() + { + Title = "Open user database file", + Filter = "Passkey user database file|*.pku", + }; + + if (!(dialog.ShowDialog() ?? false)) return; + + _resetCredentials(); + MainViewModel.Database?.Close(); + MainViewModel.Database = null; + _mainViewModel.DatabaseFile = dialog.FileName; + } + } +} \ No newline at end of file diff --git a/GUI/WPF/OSSpecific/ClipboardManager.cs b/GUI/WPF/OSSpecific/ClipboardManager.cs new file mode 100644 index 0000000..57ac1fb --- /dev/null +++ b/GUI/WPF/OSSpecific/ClipboardManager.cs @@ -0,0 +1,32 @@ +using Upsilon.Apps.Passkey.Interfaces.Utils; +using Windows.ApplicationModel.DataTransfer; + +namespace Upsilon.Apps.Passkey.GUI.WPF.OSSpecific +{ + public class ClipboardManager : IClipboardManager + { + public int RemoveAllOccurence(string[] removeList) + { + int cleanedPasswordCount = 0; + + IReadOnlyList clipboardHistory = Clipboard.GetHistoryItemsAsync().AsTask().GetAwaiter().GetResult().Items; + + foreach (ClipboardHistoryItem? item in clipboardHistory) + { + DataPackageView content = item.Content; + if (content.Contains(StandardDataFormats.Text)) + { + string text = content.GetTextAsync().AsTask().GetAwaiter().GetResult(); + + if (removeList.Any(x => x == text)) + { + _ = Clipboard.DeleteItemFromHistory(item); + cleanedPasswordCount++; + } + } + } + + return cleanedPasswordCount; + } + } +} diff --git a/GUI/WPF/Properties/PublishProfiles/FolderProfile.pubxml b/GUI/WPF/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..40f5581 --- /dev/null +++ b/GUI/WPF/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,17 @@ + + + + + Release + Any CPU + bin\Release\net10.0-windows10.0.18362.0\publish\win-x64\ + FileSystem + <_TargetId>Folder + net10.0-windows10.0.18362.0 + win-x64 + true + true + false + false + + \ No newline at end of file diff --git a/GUI/WPF/Properties/launchSettings.json b/GUI/WPF/Properties/launchSettings.json new file mode 100644 index 0000000..858325c --- /dev/null +++ b/GUI/WPF/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "Upsilon.Apps.Passkey.GUI.WPF": { + "commandName": "Project", + "commandLineArgs": "\"vidpll-sVqi93MN9TymqpQ==UGOcrUlBjHsiPMUpOVwOKjiSUBw=.pku\"", + "workingDirectory": "bin\\Debug\\net10.0-windows10.0.18362.0\\raw" + } + } +} \ No newline at end of file diff --git a/GUI/WPF/Themes/DarkMode.cs b/GUI/WPF/Themes/DarkMode.cs new file mode 100644 index 0000000..dce15eb --- /dev/null +++ b/GUI/WPF/Themes/DarkMode.cs @@ -0,0 +1,31 @@ +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; + +namespace Upsilon.Apps.Passkey.GUI.WPF.Themes +{ + public static class DarkMode + { + public static Brush UnchangedBrush1 => new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x1E)); + public static Brush UnchangedBrush2 => new SolidColorBrush(Color.FromRgb(0x2D, 0x2D, 0x30)); + public static Brush ChangedBrush => new SolidColorBrush(Color.FromRgb(0x60, 0x60, 0x60)); + + public static void SetDarkMode(Window window) + { + nint hwnd = new WindowInteropHelper(window).Handle; + + if (hwnd == IntPtr.Zero) + { + return; + } + + int attribute = 20; // DWMWA_USE_IMMERSIVE_DARK_MODE + int useImmersiveDarkMode = 1; + _ = DwmSetWindowAttribute(hwnd, attribute, ref useImmersiveDarkMode, sizeof(int)); + } + + [DllImport("dwmapi.dll", PreserveSig = true)] + private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize); + } +} diff --git a/GUI/WPF/Themes/DarkTheme.xaml b/GUI/WPF/Themes/DarkTheme.xaml new file mode 100644 index 0000000..6366eea --- /dev/null +++ b/GUI/WPF/Themes/DarkTheme.xaml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GUI/WPF/Upsilon.Apps.Passkey.GUI.WPF.csproj b/GUI/WPF/Upsilon.Apps.Passkey.GUI.WPF.csproj new file mode 100644 index 0000000..2181d1c --- /dev/null +++ b/GUI/WPF/Upsilon.Apps.Passkey.GUI.WPF.csproj @@ -0,0 +1,42 @@ + + + + WinExe + net10.0-windows10.0.18362.0 + enable + enable + true + $(AssemblyName) + Yassin Lokhat + A local stored Password Manager GUI. + 3.0.0 + 1.0.2 + 1.0.2 + icon.ico + + + + + + + + + + + + + Always + + + + + True + 9999 + + + + True + 9999 + + + diff --git a/GUI/WPF/ViewModels/AccountPasswordsWarningViewModel.cs b/GUI/WPF/ViewModels/AccountPasswordsWarningViewModel.cs new file mode 100644 index 0000000..42021f2 --- /dev/null +++ b/GUI/WPF/ViewModels/AccountPasswordsWarningViewModel.cs @@ -0,0 +1,77 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using Upsilon.Apps.Passkey.GUI.WPF.Helper; +using Upsilon.Apps.Passkey.GUI.WPF.ViewModels.Controls; +using Upsilon.Apps.Passkey.Interfaces.Enums; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels +{ + internal class AccountPasswordsWarningViewModel : INotifyPropertyChanged + { + public string Title { get; } + + public string ReadableWarningType + { + get => WarningType.ToReadableString(); + set => WarningType = EnumHelper.ActivityWarningTypeFromReadableString(value); + } + public WarningType WarningType + { + get; + set + { + if (field != value) + { + field = value; + OnPropertyChanged(nameof(ReadableWarningType)); + RefreshFilters(); + } + } + } = WarningType.PasswordUpdateReminderWarning | WarningType.PasswordLeakedWarning; + public string Text + { + get; + set + { + if (field != value) + { + field = value; + OnPropertyChanged(nameof(Text)); + RefreshFilters(); + } + } + } = ""; + + public ObservableCollection Warnings { get; set; } = []; + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public AccountPasswordsWarningViewModel() + { + Title = MainViewModel.AppTitle + " - Account Passwords Warnings"; + RefreshFilters(); + } + + public void RefreshFilters() + { + Warnings.Clear(); + + if (MainViewModel.Database?.Warnings is null) return; + + AccountPasswordWarningViewModel[] warnings = [.. MainViewModel.Database.Warnings + .Where(x => WarningType.HasFlag(x.WarningType)) + .SelectMany(x => x.Accounts?.Select(y => new AccountPasswordWarningViewModel(y, x.WarningType)) ?? []) + .Where(x => x.MeetsConditions(WarningType, Text))]; + + foreach (AccountPasswordWarningViewModel warning in warnings) + { + Warnings.Add(warning); + } + } + } +} diff --git a/GUI/WPF/ViewModels/Controls/AccountPasswordWarningViewModel.cs b/GUI/WPF/ViewModels/Controls/AccountPasswordWarningViewModel.cs new file mode 100644 index 0000000..0b477a6 --- /dev/null +++ b/GUI/WPF/ViewModels/Controls/AccountPasswordWarningViewModel.cs @@ -0,0 +1,23 @@ +using Upsilon.Apps.Passkey.GUI.WPF.Helper; +using Upsilon.Apps.Passkey.Interfaces.Enums; +using Upsilon.Apps.Passkey.Interfaces.Models; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels.Controls +{ + internal class AccountPasswordWarningViewModel(IAccount account, WarningType warningType) + { + public string ReadableWarningType => WarningType.ToReadableString(); + public string ServiceString => Account.Service.ToString() ?? string.Empty; + public string AccountString => Account.ToString() ?? string.Empty; + + public IAccount Account = account; + public WarningType WarningType { get; } = warningType; + + public bool MeetsConditions(WarningType warningType, string text) + { + return warningType.HasFlag(WarningType) + && (AccountString.Contains(text, StringComparison.CurrentCultureIgnoreCase) + || ServiceString.Contains(text, StringComparison.CurrentCultureIgnoreCase)); + } + } +} diff --git a/GUI/WPF/ViewModels/Controls/AccountViewModel.cs b/GUI/WPF/ViewModels/Controls/AccountViewModel.cs new file mode 100644 index 0000000..35e1cbf --- /dev/null +++ b/GUI/WPF/ViewModels/Controls/AccountViewModel.cs @@ -0,0 +1,247 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Windows.Media; +using Upsilon.Apps.Passkey.GUI.WPF.Themes; +using Upsilon.Apps.Passkey.Interfaces.Enums; +using Upsilon.Apps.Passkey.Interfaces.Models; +using Upsilon.Apps.Passkey.Interfaces.Utils; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels.Controls +{ + public class AccountViewModel(IAccount account) : INotifyPropertyChanged + { + public readonly IAccount Account = account; + + public string AccountDisplay + { + get + { + string accountDisplay = $"{Account.Label} {Account.Identifiers.First()}"; + return $"{(Account.HasChanged() ? "* " : string.Empty)}{accountDisplay.Trim()}"; + } + } + + public string AccountId => $"Account Id : {Account.ItemId.Replace(Account.Service.ItemId, string.Empty)}"; + + public Brush LabelBackground => Account.HasChanged(nameof(Label)) ? DarkMode.ChangedBrush : DarkMode.UnchangedBrush2; + public string Label + { + get => Account.Label; + set + { + if (Account.Label != value) + { + Account.Label = value; + OnPropertyChanged(nameof(Label)); + } + } + } + + public ObservableCollection Identifiers = []; + + public Brush PasswordBackground => Account.HasChanged(nameof(Password)) ? DarkMode.ChangedBrush : !PasswordLeaked ? DarkMode.UnchangedBrush2 : Brushes.Red; + public string Password + { + get => Account.Password; + set + { + if (Account.Password != value) + { + Account.Password = value; + OnPropertyChanged(nameof(Password)); + } + } + } + + public PasswordViewModel[] Passwords + { + get + { + PasswordViewModel[] passwords = [.. Account.Passwords + .OrderByDescending(x => x.Key) + .Select(x => new PasswordViewModel(x.Key.ToShortDateString(), x.Value))]; + + if (passwords.Length != 0 + && string.IsNullOrEmpty(passwords.Last().Password)) + { + passwords = passwords[..(passwords.Length - 1)]; + } + + return passwords; + } + } + + public Brush NotesBackground => Account.HasChanged(nameof(Notes)) ? DarkMode.ChangedBrush : DarkMode.UnchangedBrush2; + public string Notes + { + get => Account.Notes; + set + { + if (Account.Notes != value) + { + Account.Notes = value; + OnPropertyChanged(nameof(Notes)); + } + } + } + + public int RemindPasswordUpdateDelay + { + get => Account.PasswordUpdateReminderDelay; + set + { + if (Account.PasswordUpdateReminderDelay != value) + { + Account.PasswordUpdateReminderDelay = value; + + OnPropertyChanged(nameof(RemindPasswordUpdateDelay)); + OnPropertyChanged(nameof(RemindPasswordUpdate)); + } + } + } + + public bool RemindPasswordUpdate + { + get => RemindPasswordUpdateDelay != 0; + set + { + if (RemindPasswordUpdate != value) + { + RemindPasswordUpdateDelay = value ? 2 : 0; + OnPropertyChanged(nameof(RemindPasswordUpdate)); + } + } + } + + public bool WarnPasswordLeak + { + get => Account.Options.HasFlag(AccountOption.WarnIfPasswordLeaked); + set + { + if (WarnPasswordLeak != value) + { + if (value) + { + Account.Options |= AccountOption.WarnIfPasswordLeaked; + } + else + { + Account.Options &= ~AccountOption.WarnIfPasswordLeaked; + } + + OnPropertyChanged(nameof(WarnPasswordLeak)); + } + } + } + + public bool WarnIfDuplicatedPassword + { + get => Account.Options.HasFlag(AccountOption.WarnIfDuplicatedPassword); + set + { + if (WarnIfDuplicatedPassword != value) + { + if (value) + { + Account.Options |= AccountOption.WarnIfDuplicatedPassword; + } + else + { + Account.Options &= ~AccountOption.WarnIfDuplicatedPassword; + } + + OnPropertyChanged(nameof(WarnIfDuplicatedPassword)); + } + } + } + + public bool PasswordLeaked + => Account.Options.HasFlag(AccountOption.WarnIfPasswordLeaked) + && MainViewModel.Database?.Warnings is not null + && MainViewModel.Database.Warnings.Any(x => x.WarningType == WarningType.PasswordLeakedWarning + && x.Accounts.Contains(Account)); + + public static string[] IdentifierAutoCompleteList => MainViewModel.User?.Services + .SelectMany(x => x.Accounts) + .SelectMany(x => x.Identifiers) + .Distinct() + .Where(x => !string.IsNullOrEmpty(x)) + .OrderBy(x => x) + .ToArray() ?? []; + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{propertyName}Background")); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AccountDisplay))); + } + + private void _identifierViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName != "Identifier") + { + return; + } + + Account.Identifiers = [.. Identifiers.Select(x => x.Identifier)]; + + foreach (IdentifierViewModel? identifier in Identifiers.Except([sender]).Cast()) + { + identifier?.Refresh(); + } + + OnPropertyChanged(string.Empty); + } + + public void AddIdentifier(IdentifierViewModel identifierViewModel) + { + identifierViewModel.PropertyChanged += _identifierViewModel_PropertyChanged; + + _identifierViewModel_PropertyChanged(null, new("Identifier")); + } + + public void AddIdentifier(string identifier) + { + IdentifierViewModel identifierViewModel = new(Account, identifier); + identifierViewModel.PropertyChanged += _identifierViewModel_PropertyChanged; + + Identifiers.Add(identifierViewModel); + + _identifierViewModel_PropertyChanged(null, new("Identifier")); + } + + public bool RemoveIdentifier(IdentifierViewModel identifierViewModel) + { + if (Identifiers.Count == 1) + { + return false; + } + + _ = Identifiers.Remove(identifierViewModel); + + _identifierViewModel_PropertyChanged(null, new("Identifier")); + + return true; + } + + public bool MoveIdentifier(int oldIndex, int newIndex) + { + if (oldIndex < 0 + || newIndex < 0 + || newIndex >= Identifiers.Count) + { + return false; + } + + (Identifiers[newIndex], Identifiers[oldIndex]) = (Identifiers[oldIndex], Identifiers[newIndex]); + + _identifierViewModel_PropertyChanged(null, new("Identifier")); + + return true; + } + + public override string ToString() => $"{(Account.HasChanged() ? "* " : string.Empty)}{Account}"; + } +} diff --git a/GUI/WPF/ViewModels/Controls/ActivityViewModel.cs b/GUI/WPF/ViewModels/Controls/ActivityViewModel.cs new file mode 100644 index 0000000..2a7fec2 --- /dev/null +++ b/GUI/WPF/ViewModels/Controls/ActivityViewModel.cs @@ -0,0 +1,46 @@ +using System.ComponentModel; +using Upsilon.Apps.Passkey.GUI.WPF.Helper; +using Upsilon.Apps.Passkey.Interfaces.Enums; +using Upsilon.Apps.Passkey.Interfaces.Models; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels.Controls +{ + internal class ActivityViewModel(IActivity activity) : INotifyPropertyChanged + { + public readonly IActivity Activity = activity; + public string DateTime => Activity.DateTime.ToString("yyyy-MM-dd HH:mm"); + public string EventType => Activity.EventType.ToReadableString(); + public string Message => Activity.Message; + public bool NeedsReview + { + get => Activity.NeedsReview; + set + { + if (Activity.NeedsReview != value) + { + Activity.NeedsReview = value; + OnPropertyChanged(nameof(NeedsReview)); + OnPropertyChanged(nameof(NeedsReviewString)); + } + } + } + public string NeedsReviewString => NeedsReview ? "Needs review" : "Reviewed"; + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public bool MeetsConditions(string itemId, DateTime fromDateFilter, DateTime toDateFilter, ActivityEventType eventType, string message, bool needsReview) + { + return (string.IsNullOrEmpty(itemId) || Activity.ItemId == itemId) + && (fromDateFilter > System.DateTime.Now.Date + || Activity.DateTime.Date >= fromDateFilter) && (toDateFilter > System.DateTime.Now.Date + || Activity.DateTime.Date <= toDateFilter) && (eventType == ActivityEventType.None + || Activity.EventType == eventType) && (!needsReview + || Activity.NeedsReview) && Activity.Message.Contains(message, StringComparison.CurrentCultureIgnoreCase); + } + } +} \ No newline at end of file diff --git a/GUI/WPF/ViewModels/Controls/DuplicatedPasswordWarningViewModel.cs b/GUI/WPF/ViewModels/Controls/DuplicatedPasswordWarningViewModel.cs new file mode 100644 index 0000000..3ef90b8 --- /dev/null +++ b/GUI/WPF/ViewModels/Controls/DuplicatedPasswordWarningViewModel.cs @@ -0,0 +1,18 @@ +using Upsilon.Apps.Passkey.Interfaces.Models; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels.Controls +{ + internal class DuplicatedPasswordWarningViewModel + { + private readonly IWarning _warning; + + public string DuplicatedPassword => $"{_warning.Accounts?.Length} accounts with same passwords"; + public AccountPasswordWarningViewModel[] Accounts { get; set; } + + public DuplicatedPasswordWarningViewModel(IWarning warning) + { + _warning = warning; + Accounts = [.. _warning.Accounts?.Select(x => new AccountPasswordWarningViewModel(x, _warning.WarningType)) ?? []]; + } + } +} diff --git a/GUI/WPF/ViewModels/Controls/IdentifiantViewModel.cs b/GUI/WPF/ViewModels/Controls/IdentifiantViewModel.cs new file mode 100644 index 0000000..c483e8a --- /dev/null +++ b/GUI/WPF/ViewModels/Controls/IdentifiantViewModel.cs @@ -0,0 +1,77 @@ +using System.ComponentModel; +using System.Text.RegularExpressions; +using System.Windows.Media; +using Upsilon.Apps.Passkey.GUI.WPF.Themes; +using Upsilon.Apps.Passkey.Interfaces.Models; +using Upsilon.Apps.Passkey.Interfaces.Utils; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels.Controls +{ + public partial class IdentifierViewModel : INotifyPropertyChanged + { + private readonly IAccount _account; + + public static readonly Dictionary IdentifiersTypes = new() + { + { "[Username]", "👤" }, + { "[Email]", "📧" }, + { "[Phone Number]", "🖁" }, + { "[Passkey]", "🗝" }, + { "[Authentificator App]", "📲" }, + }; + + public Brush IdentifierBackground => _account.HasChanged("Identifiers") ? DarkMode.ChangedBrush : DarkMode.UnchangedBrush2; + + public string Identifier + { + get; + set + { + if (field != value) + { + if (IdentifiersTypes.Keys.Union(IdentifiersTypes.Values).All(x => !value.StartsWith(x, StringComparison.CurrentCultureIgnoreCase))) + { + value = _getIdentifierType(value); + } + + foreach (KeyValuePair idType in IdentifiersTypes) + { + field = value.Replace(idType.Key, idType.Value); + } + + OnPropertyChanged(nameof(Identifier)); + } + } + } = string.Empty; + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{propertyName}Background")); + } + + public IdentifierViewModel(IAccount account, string identifier) + { + _account = account; + Identifier = identifier; + } + + public void Refresh() + { + OnPropertyChanged(nameof(IdentifierBackground)); + } + + [GeneratedRegex(@"^\+\d{1,3}[\d\s\-\.]{6,20}$")] + private static partial Regex _phoneRegex(); + [GeneratedRegex(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")] + private static partial Regex _mailRegex(); + private static string _getIdentifierType(string identifier) + { + return _phoneRegex().IsMatch(identifier) ? "🖁" + identifier + : _mailRegex().IsMatch(identifier) ? "📧" + identifier + : "👤" + identifier; + } + } +} diff --git a/GUI/WPF/ViewModels/Controls/PasswordViewModel.cs b/GUI/WPF/ViewModels/Controls/PasswordViewModel.cs new file mode 100644 index 0000000..7191f12 --- /dev/null +++ b/GUI/WPF/ViewModels/Controls/PasswordViewModel.cs @@ -0,0 +1,17 @@ +using System.ComponentModel; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels.Controls +{ + public class PasswordViewModel(string updateDate, string password) : INotifyPropertyChanged + { + public string UpdateDate { get; set; } = updateDate; + public string Password { get; set; } = password; + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/GUI/WPF/ViewModels/Controls/ServiceViewModel.cs b/GUI/WPF/ViewModels/Controls/ServiceViewModel.cs new file mode 100644 index 0000000..39a434e --- /dev/null +++ b/GUI/WPF/ViewModels/Controls/ServiceViewModel.cs @@ -0,0 +1,126 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Windows.Media; +using Upsilon.Apps.Passkey.GUI.WPF.Helper; +using Upsilon.Apps.Passkey.GUI.WPF.Themes; +using Upsilon.Apps.Passkey.Interfaces.Models; +using Upsilon.Apps.Passkey.Interfaces.Utils; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels.Controls +{ + internal class ServiceViewModel : INotifyPropertyChanged + { + public readonly IService Service; + + public string ServiceDisplay => $"{(Service.HasChanged() ? "* " : string.Empty)}{Service.ServiceName}"; + + public string ServiceId => $"Service Id : {Service.ItemId.Replace(Service.User.ItemId, string.Empty)}"; + + public Brush ServiceNameBackground => Service.HasChanged(nameof(ServiceName)) ? DarkMode.ChangedBrush : DarkMode.UnchangedBrush2; + public string ServiceName + { + get => Service.ServiceName; + set + { + if (Service.ServiceName != value) + { + Service.ServiceName = value; + OnPropertyChanged(nameof(ServiceName)); + } + } + } + + public Brush UrlBackground => Service.HasChanged(nameof(Url)) ? DarkMode.ChangedBrush : DarkMode.UnchangedBrush2; + public string Url + { + get => Service.Url; + set + { + if (Service.Url != value) + { + Service.Url = value; + OnPropertyChanged(nameof(Url)); + } + } + } + + public Brush NotesBackground => Service.HasChanged(nameof(Notes)) ? DarkMode.ChangedBrush : DarkMode.UnchangedBrush2; + public string Notes + { + get => Service.Notes; + set + { + if (Service.Notes != value) + { + Service.Notes = value; + OnPropertyChanged(nameof(Notes)); + } + } + } + + public ObservableCollection Accounts = []; + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{propertyName}Background")); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ServiceDisplay))); + } + + public ServiceViewModel(IService service, string identifierFilter = "", string textFilter = "", bool changedItemsOnly = false) + { + Service = service; + + IAccount[] accounts = [.. Service.Accounts.Where(x => x.MeetsFilterConditions(identifierFilter, textFilter, changedItemsOnly))]; + + if (accounts.Length == 0) + { + accounts = Service.Accounts; + } + + foreach (IAccount account in accounts) + { + AccountViewModel accountViewModel = new(account); + accountViewModel.PropertyChanged += _accountViewModel_PropertyChanged; + Accounts.Add(accountViewModel); + } + } + + public AccountViewModel AddAccount() + { + AccountViewModel? accountViewModel = Accounts.FirstOrDefault(x => x.Identifiers.Any(y => y.Identifier.StartsWith("👤New Account #"))); + + if (accountViewModel is null) + { + accountViewModel = new(Service.AddAccount(["👤New Account #" + DateTime.Now.Ticks])); + accountViewModel.PropertyChanged += _accountViewModel_PropertyChanged; + Accounts.Insert(0, accountViewModel); + + OnPropertyChanged(string.Empty); + } + + return accountViewModel; + } + + public int DeleteAccount(AccountViewModel accountViewModel) + { + int index = Accounts.IndexOf(accountViewModel); + + _ = Accounts.Remove(accountViewModel); + Service.DeleteAccount(accountViewModel.Account); + + OnPropertyChanged(string.Empty); + + return index < Accounts.Count ? index : Accounts.Count - 1; + } + + private void _accountViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + OnPropertyChanged(string.Empty); + } + + public override string ToString() => $"{(Service.HasChanged() ? "* " : string.Empty)}{Service}"; + } +} diff --git a/GUI/WPF/ViewModels/Controls/UserPasswordItemViewModel.cs b/GUI/WPF/ViewModels/Controls/UserPasswordItemViewModel.cs new file mode 100644 index 0000000..3740618 --- /dev/null +++ b/GUI/WPF/ViewModels/Controls/UserPasswordItemViewModel.cs @@ -0,0 +1,26 @@ +using System.ComponentModel; +using Upsilon.Apps.Passkey.GUI.WPF.Helper; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels.Controls +{ + public class UserPasswordItemViewModel : INotifyPropertyChanged + { + public int Index + { + get; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = 0; + public string Password + { + get; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = string.Empty; + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/GUI/WPF/ViewModels/Controls/VisiblePasswordBoxViewModel.cs b/GUI/WPF/ViewModels/Controls/VisiblePasswordBoxViewModel.cs new file mode 100644 index 0000000..0e47970 --- /dev/null +++ b/GUI/WPF/ViewModels/Controls/VisiblePasswordBoxViewModel.cs @@ -0,0 +1,67 @@ +using System.ComponentModel; +using System.Windows; +using System.Windows.Media; +using Upsilon.Apps.Passkey.GUI.WPF.Helper; +using Upsilon.Apps.Passkey.GUI.WPF.Themes; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels.Controls +{ + public class VisiblePasswordBoxViewModel : INotifyPropertyChanged + { + public string Password + { + get; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = string.Empty; + public Visibility PasswordVisibility + { + get; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = Visibility.Visible; + public Visibility TextVisibility + { + get; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = Visibility.Collapsed; + public Visibility ButtonVisibility + { + get; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = Visibility.Visible; + + public bool IsEnabled + { + get; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = true; + + public Brush Background + { + get; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = DarkMode.UnchangedBrush2; + + public bool PasswordIsVisible => PasswordVisibility == System.Windows.Visibility.Collapsed; + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public void TooglePasswordVisibility() + { + if (!PasswordIsVisible) + { + PasswordVisibility = System.Windows.Visibility.Collapsed; + TextVisibility = System.Windows.Visibility.Visible; + } + else + { + PasswordVisibility = System.Windows.Visibility.Visible; + TextVisibility = System.Windows.Visibility.Collapsed; + } + } + } +} diff --git a/GUI/WPF/ViewModels/DuplicatedPasswordsWarningViewModel.cs b/GUI/WPF/ViewModels/DuplicatedPasswordsWarningViewModel.cs new file mode 100644 index 0000000..82152a0 --- /dev/null +++ b/GUI/WPF/ViewModels/DuplicatedPasswordsWarningViewModel.cs @@ -0,0 +1,22 @@ +using Upsilon.Apps.Passkey.GUI.WPF.ViewModels.Controls; +using Upsilon.Apps.Passkey.Interfaces.Enums; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels +{ + internal class DuplicatedPasswordsWarningViewModel + { + public string Title { get; } + + public DuplicatedPasswordWarningViewModel[] Warnings { get; set; } + + public DuplicatedPasswordsWarningViewModel() + { + Title = MainViewModel.AppTitle + " - Duplicated Passwords Warnings"; + + Warnings = [.. MainViewModel.Database?.Warnings? + .Where(x => x.WarningType == WarningType.DuplicatedPasswordsWarning) + .Select(x => new DuplicatedPasswordWarningViewModel(x)) + ?? []]; + } + } +} diff --git a/GUI/WPF/ViewModels/InsertIdentifierViewModel.cs b/GUI/WPF/ViewModels/InsertIdentifierViewModel.cs new file mode 100644 index 0000000..4a4f724 --- /dev/null +++ b/GUI/WPF/ViewModels/InsertIdentifierViewModel.cs @@ -0,0 +1,46 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using Upsilon.Apps.Passkey.GUI.WPF.Helper; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels +{ + internal class InsertIdentifierViewModel(IEnumerable identifiers, string identifier) : INotifyPropertyChanged + { + private readonly string[] _identifiers = [.. identifiers]; + + public ObservableCollection Identifiers = [.. identifiers.Where(x => x.StartsWith(identifier.Trim(), StringComparison.CurrentCultureIgnoreCase)), + .. identifiers.Where(x => x.Contains(identifier.Trim(), StringComparison.CurrentCultureIgnoreCase) + && !x.StartsWith(identifier.Trim(), StringComparison.CurrentCultureIgnoreCase))]; + + public string Identifier + { + get => field.Trim(); + set + { + _ = PropertyHelper.SetProperty(ref field, value.Trim(), this, PropertyChanged); + _refreshFilter(); + } + } = identifier; + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + private void _refreshFilter() + { + Identifiers.Clear(); + + string[] identifiers = [.. _identifiers.Where(x => x.StartsWith(Identifier, StringComparison.CurrentCultureIgnoreCase)), + .. _identifiers.Where(x => x.Contains(Identifier, StringComparison.CurrentCultureIgnoreCase) + && !x.StartsWith(Identifier, StringComparison.CurrentCultureIgnoreCase))]; + + foreach (string identifier in identifiers) + { + Identifiers.Add(identifier); + } + } + } +} diff --git a/GUI/WPF/ViewModels/MainViewModel.cs b/GUI/WPF/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..fffdc34 --- /dev/null +++ b/GUI/WPF/ViewModels/MainViewModel.cs @@ -0,0 +1,60 @@ +using System.ComponentModel; +using System.IO; +using Upsilon.Apps.Passkey.Core.Utils; +using Upsilon.Apps.Passkey.GUI.WPF.Helper; +using Upsilon.Apps.Passkey.GUI.WPF.OSSpecific; +using Upsilon.Apps.Passkey.GUI.WPF.Views; +using Upsilon.Apps.Passkey.Interfaces.Models; +using Upsilon.Apps.Passkey.Interfaces.Utils; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels +{ + internal class MainViewModel : INotifyPropertyChanged + { + public static string AppTitle + { + get + { + System.Reflection.AssemblyName package = System.Reflection.Assembly.GetExecutingAssembly().GetName(); + string? packageVersion = package.Version?.ToString(3); + + return $"{package.Name} v{packageVersion}"; + } + } + + public static readonly ICryptographyCenter CryptographyCenter = new CryptographyCenter(); + public static readonly ISerializationCenter SerializationCenter = new JsonSerializationCenter(); + public static readonly IPasswordFactory PasswordFactory = new PasswordFactory(); + public static readonly IClipboardManager ClipboardManager = new ClipboardManager(); + + public static IDatabase? Database = null; + + public static IUser User => Database is null || Database.User is null ? throw new NullReferenceException(nameof(User)) : Database.User; + + public static AccountPasswordsWarningView? AccountPasswordsWarningView; + public static DuplicatedPasswordsWarningView? DuplicatedPasswordsWarningView; + public static UserActivitiesView? UserActivitiesView; + public static Action? GoToItem; + + public string DatabaseFile + { + get; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged, nameof(DatabaseLabel)); + } = string.Empty; + + public string DatabaseLabel => File.Exists(DatabaseFile) ? $"Database : {Path.GetFileName(DatabaseFile)}" : "No database loaded."; + + public string CredentialsLabel + { + get; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = "Username :"; + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/GUI/WPF/ViewModels/PasswordGeneratorViewModel.cs b/GUI/WPF/ViewModels/PasswordGeneratorViewModel.cs new file mode 100644 index 0000000..e4d7834 --- /dev/null +++ b/GUI/WPF/ViewModels/PasswordGeneratorViewModel.cs @@ -0,0 +1,168 @@ +using System.ComponentModel; +using System.Text; +using Upsilon.Apps.Passkey.GUI.WPF.Helper; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels +{ + internal class PasswordGeneratorViewModel : INotifyPropertyChanged + { + public static string Title => MainViewModel.AppTitle + " - Password Generator"; + + public bool CheckIfLeaked + { + get; + set + { + if (field != value) + { + field = value; + _includeCharactersChanged(nameof(CheckIfLeaked)); + } + } + } = true; + + public int PasswordLength + { + get; + set + { + if (field != value) + { + field = value; + OnPropertyChanged(nameof(PasswordLength)); + GeneratePassword(); + } + } + } = 20; + + public string GeneratedPassword + { + get; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = string.Empty; + + public bool IncludeNumerics + { + get; + set + { + if (field != value) + { + field = value; + _includeCharactersChanged(nameof(IncludeNumerics)); + } + } + } = true; + + public bool IncludeSpecialCharacters + { + get; + set + { + if (field != value) + { + field = value; + _includeCharactersChanged(nameof(IncludeSpecialCharacters)); + } + } + } = true; + + public bool IncludeLowerCaseAlphabet + { + get; + set + { + if (field != value) + { + field = value; + _includeCharactersChanged(nameof(IncludeLowerCaseAlphabet)); + } + } + } = true; + + public bool IncludeUpperCaseAlphabet + { + get; + set + { + if (field != value) + { + field = value; + _includeCharactersChanged(nameof(IncludeUpperCaseAlphabet)); + } + } + } = true; + + private string _alphabet; + public string Alphabet + { + get => _alphabet; + set + { + if (_alphabet != value) + { + _alphabet = value; + OnPropertyChanged(nameof(Alphabet)); + GeneratePassword(); + } + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public PasswordGeneratorViewModel() + { + _alphabet = _buildAlphabet(); + GeneratePassword(); + } + + internal void GeneratePassword() + { + GeneratedPassword = string.Empty; + + _ = Task.Run(() => + { + GeneratedPassword = MainViewModel.PasswordFactory.GeneratePassword(PasswordLength, Alphabet, CheckIfLeaked); + }); + } + + private void _includeCharactersChanged(string propertyName) + { + OnPropertyChanged(propertyName); + + Alphabet = _buildAlphabet(); + } + + private string _buildAlphabet() + { + StringBuilder alphabetBuilder = new(); + + if (IncludeNumerics) + { + _ = alphabetBuilder.Append(MainViewModel.PasswordFactory.Numeric); + } + + if (IncludeUpperCaseAlphabet) + { + _ = alphabetBuilder.Append(MainViewModel.PasswordFactory.Alphabetic.ToUpper()); + } + + if (IncludeLowerCaseAlphabet) + { + _ = alphabetBuilder.Append(MainViewModel.PasswordFactory.Alphabetic.ToLower()); + } + + if (IncludeSpecialCharacters) + { + _ = alphabetBuilder.Append(MainViewModel.PasswordFactory.SpecialChars); + } + + return alphabetBuilder.ToString(); + } + } +} diff --git a/GUI/WPF/ViewModels/UserActivitiesViewModel.cs b/GUI/WPF/ViewModels/UserActivitiesViewModel.cs new file mode 100644 index 0000000..1bddfb6 --- /dev/null +++ b/GUI/WPF/ViewModels/UserActivitiesViewModel.cs @@ -0,0 +1,140 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using Upsilon.Apps.Passkey.GUI.WPF.Helper; +using Upsilon.Apps.Passkey.GUI.WPF.ViewModels.Controls; +using Upsilon.Apps.Passkey.Interfaces.Enums; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels +{ + internal class UserActivitiesViewModel : INotifyPropertyChanged + { + public string Title { get; } + + public string FiltersHeader => $"Filters : {Activities.Count} activities found over {MainViewModel.Database?.Activities?.Length}"; + public DateTime FromDateFilter + { + get; + set + { + if (field != value) + { + field = value; + OnPropertyChanged(nameof(FromDateFilter)); + RefreshFilters(); + } + } + } = DateTime.Now.Date.AddDays(1); + public DateTime ToDateFilter + { + get; + set + { + if (field != value) + { + field = value; + OnPropertyChanged(nameof(ToDateFilter)); + RefreshFilters(); + } + } + } = DateTime.Now.Date.AddDays(1); + + public string ReadableEventType + { + get => EventType.ToReadableString(); + set => EventType = EnumHelper.ActivityEventTypeFromReadableString(value); + } + public ActivityEventType EventType + { + get; + set + { + if (field != value) + { + field = value; + OnPropertyChanged(nameof(ReadableEventType)); + RefreshFilters(); + } + } + } = ActivityEventType.None; + + public string Message + { + get; + set + { + if (field != value) + { + field = value; + OnPropertyChanged(nameof(Message)); + RefreshFilters(); + } + } + } = ""; + + public bool NeedsReview + { + get; + set + { + if (field != value) + { + field = value; + OnPropertyChanged(nameof(NeedsReview)); + RefreshFilters(); + } + } + } = false; + + public ObservableCollection Activities { get; set; } = []; + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public UserActivitiesViewModel() + { + Title = MainViewModel.AppTitle + " - Activities"; + + RefreshFilters(); + } + + private bool _locked = false; + public void ClearFilters() + { + _locked = true; + + FromDateFilter = ToDateFilter = DateTime.Now.Date.AddDays(1); + EventType = ActivityEventType.None; + Message = string.Empty; + NeedsReview = false; + + _locked = false; + + RefreshFilters(); + } + + public void RefreshFilters(string itemId = "") + { + if (_locked) return; + + Activities.Clear(); + + if (MainViewModel.Database?.Activities is null) return; + + ActivityViewModel[] activities = [.. MainViewModel.Database.Activities + .Select(x => new ActivityViewModel(x)) + .Where(x => x.MeetsConditions(itemId, FromDateFilter, ToDateFilter, EventType, Message, NeedsReview)) + .OrderByDescending(x => x.DateTime)]; + + foreach (ActivityViewModel activity in activities) + { + Activities.Add(activity); + } + + OnPropertyChanged(nameof(FiltersHeader)); + } + } +} diff --git a/GUI/WPF/ViewModels/UserServicesViewModel.cs b/GUI/WPF/ViewModels/UserServicesViewModel.cs new file mode 100644 index 0000000..be2a84e --- /dev/null +++ b/GUI/WPF/ViewModels/UserServicesViewModel.cs @@ -0,0 +1,200 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Windows.Media; +using System.Windows.Threading; +using Upsilon.Apps.Passkey.GUI.WPF.Helper; +using Upsilon.Apps.Passkey.GUI.WPF.ViewModels.Controls; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels +{ + internal class UserServicesViewModel : INotifyPropertyChanged + { + private readonly string _defaultTitle; + + public string Title + { + get => field; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } + + public string ShowWarnings + { + get => field; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = string.Empty; + + public Brush ShowWarningsColor + { + get => field; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = Brushes.White; + + public string ShowActivityWarnings + { + get => field; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = string.Empty; + + public string ShowExpiredPasswordWarnings + { + get => field; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = string.Empty; + + public string ShowDuplicatedPasswordWarnings + { + get => field; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = string.Empty; + + public string ShowLeakedPasswordWarnings + { + get => field; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = string.Empty; + + public string ServiceFilter + { + get; + set + { + if (field != value) + { + field = value; + OnPropertyChanged(nameof(ServiceFilter)); + + RefreshFilters(); + } + } + } = string.Empty; + + public string IdentifierFilter + { + get; + set + { + if (field != value) + { + field = value; + OnPropertyChanged(nameof(IdentifierFilter)); + + RefreshFilters(); + } + } + } = string.Empty; + + public string TextFilter + { + get; + set + { + if (field != value) + { + field = value; + OnPropertyChanged(nameof(TextFilter)); + + RefreshFilters(); + } + } + } = string.Empty; + + public bool ChangedItemsOnly + { + get; + set + { + if (field != value) + { + field = value; + OnPropertyChanged(nameof(ChangedItemsOnly)); + + RefreshFilters(); + } + } + } = false; + + public ObservableCollection Services { get; set; } = []; + + public event PropertyChangedEventHandler? PropertyChanged; + + public event EventHandler? FiltersRefreshed; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public UserServicesViewModel(string defaultTitle) + { + Title = _defaultTitle = defaultTitle; + + RefreshFilters(); + + DispatcherTimer timer = new() + { + Interval = new TimeSpan(0, 0, 0, 0, 500), + IsEnabled = true, + }; + + timer.Tick += _timer_Elapsed; + } + + public ServiceViewModel AddService() + { + ServiceViewModel? serviceViewModel = Services.FirstOrDefault(x => x.ServiceName.StartsWith("New Service #")); + + if (serviceViewModel is null) + { + serviceViewModel = new(MainViewModel.User.AddService("New Service #" + DateTime.Now.Ticks)); + Services.Insert(0, serviceViewModel); + } + + return serviceViewModel; + } + + public int DeleteService(ServiceViewModel serviceViewModel) + { + int index = Services.IndexOf(serviceViewModel); + + _ = Services.Remove(serviceViewModel); + MainViewModel.User.DeleteService(serviceViewModel.Service); + + return index < Services.Count ? index : Services.Count - 1; + } + + public void RefreshFilters() + { + Services.Clear(); + + ServiceViewModel[] services = [.. MainViewModel.User.Services + .Where(x => x.MeetsFilterConditions(ServiceFilter, IdentifierFilter, TextFilter, ChangedItemsOnly)) + .OrderBy(x => x.ServiceName) + .Select(x => new ServiceViewModel(x, IdentifierFilter, TextFilter, ChangedItemsOnly))]; + + foreach (ServiceViewModel service in services) + { + Services.Add(service); + } + + FiltersRefreshed?.Invoke(this, EventArgs.Empty); + } + + private void _timer_Elapsed(object? sender, EventArgs e) + { + string title = _defaultTitle; + + if (MainViewModel.Database?.User is not null) + { + if (MainViewModel.Database.User.HasChanged()) + { + title += " - *"; + } + + int sessionLeftTime = MainViewModel.Database.SessionLeftTime ?? 0; + title += $" - Left session time : {sessionLeftTime / 60:D2}:{sessionLeftTime % 60:D2}"; + } + + Title = title; + } + } +} diff --git a/GUI/WPF/ViewModels/UserSettingsViewModel.cs b/GUI/WPF/ViewModels/UserSettingsViewModel.cs new file mode 100644 index 0000000..0fd692b --- /dev/null +++ b/GUI/WPF/ViewModels/UserSettingsViewModel.cs @@ -0,0 +1,196 @@ +using System.ComponentModel; +using Upsilon.Apps.Passkey.GUI.WPF.Helper; + +namespace Upsilon.Apps.Passkey.GUI.WPF.ViewModels +{ + internal class UserSettingsViewModel : INotifyPropertyChanged + { + public string Title { get; } + public string Username + { + get; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = "NewUser"; + public int LogoutTimeout + { + get; + set + { + if (field != value) + { + field = value; + + OnPropertyChanged(nameof(LogoutTimeout)); + OnPropertyChanged(nameof(LogoutTimeoutChecked)); + } + } + } = 5; + public bool LogoutTimeoutChecked + { + get => LogoutTimeout != 0; + set + { + if (LogoutTimeoutChecked != value) + { + LogoutTimeout = value ? 5 : 0; + OnPropertyChanged(nameof(LogoutTimeoutChecked)); + } + } + } + public int CleaningClipboardTimeout + { + get; + set + { + if (field != value) + { + field = value; + + OnPropertyChanged(nameof(CleaningClipboardTimeout)); + OnPropertyChanged(nameof(CleaningClipboardTimeoutChecked)); + } + } + } = 30; + public bool CleaningClipboardTimeoutChecked + { + get => CleaningClipboardTimeout != 0; + set + { + if (CleaningClipboardTimeoutChecked != value) + { + CleaningClipboardTimeout = value ? 30 : 0; + OnPropertyChanged(nameof(CleaningClipboardTimeoutChecked)); + } + } + } + public int ShowPasswordDelay + { + get; + set + { + if (field != value) + { + field = value; + OnPropertyChanged(nameof(ShowPasswordDelay)); + OnPropertyChanged(nameof(ShowPasswordDelayChecked)); + } + } + } = 500; + public bool ShowPasswordDelayChecked + { + get => ShowPasswordDelay != 0; + set + { + if (ShowPasswordDelayChecked != value) + { + ShowPasswordDelay = value ? 500 : 0; + OnPropertyChanged(nameof(ShowPasswordDelayChecked)); + } + } + } + public int NumberOfOldPasswordToKeep + { + get; + set + { + if (field != value) + { + field = value; + OnPropertyChanged(nameof(NumberOfOldPasswordToKeep)); + OnPropertyChanged(nameof(NumberOfOldPasswordToKeepChecked)); + } + } + } = 0; + public bool NumberOfOldPasswordToKeepChecked + { + get => NumberOfOldPasswordToKeep != 0; + set + { + if (NumberOfOldPasswordToKeepChecked != value) + { + NumberOfOldPasswordToKeep = value ? 10 : 0; + OnPropertyChanged(nameof(NumberOfOldPasswordToKeepChecked)); + } + } + } + public int NumberOfMonthActivitiesToKeep + { + get; + set + { + if (field != value) + { + field = value; + OnPropertyChanged(nameof(NumberOfMonthActivitiesToKeep)); + OnPropertyChanged(nameof(NumberOfMonthActivitiesToKeepChecked)); + } + } + } = 0; + public bool NumberOfMonthActivitiesToKeepChecked + { + get => NumberOfMonthActivitiesToKeep != 0; + set + { + if (NumberOfMonthActivitiesToKeepChecked != value) + { + NumberOfMonthActivitiesToKeep = value ? 12 : 0; + OnPropertyChanged(nameof(NumberOfMonthActivitiesToKeepChecked)); + } + } + } + public bool NotifyActivityReview + { + get; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = true; + public bool NotifyPasswordUpdateReminder + { + get; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = true; + public bool NotifyDuplicatedPasswords + { + get; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = true; + public bool NotifyPasswordLeaked + { + get; + set => PropertyHelper.SetProperty(ref field, value, this, PropertyChanged); + } = true; + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public UserSettingsViewModel() + { + Title = MainViewModel.AppTitle; + + if (MainViewModel.Database is null || MainViewModel.Database.User is null) + { + Title += " - New user"; + } + else + { + Title += " - User settings"; + + Username = MainViewModel.Database.User.Username; + + LogoutTimeout = MainViewModel.Database.User.LogoutTimeout; + CleaningClipboardTimeout = MainViewModel.Database.User.CleaningClipboardTimeout; + ShowPasswordDelay = MainViewModel.Database.User.ShowPasswordDelay; + NumberOfOldPasswordToKeep = MainViewModel.Database.User.NumberOfOldPasswordToKeep; + NumberOfMonthActivitiesToKeep = MainViewModel.Database.User.NumberOfMonthActivitiesToKeep; + + NotifyActivityReview = (MainViewModel.Database.User.WarningsToNotify & Passkey.Interfaces.Enums.WarningType.ActivityReviewWarning) != 0; + NotifyPasswordUpdateReminder = (MainViewModel.Database.User.WarningsToNotify & Passkey.Interfaces.Enums.WarningType.PasswordUpdateReminderWarning) != 0; + NotifyDuplicatedPasswords = (MainViewModel.Database.User.WarningsToNotify & Passkey.Interfaces.Enums.WarningType.DuplicatedPasswordsWarning) != 0; + NotifyPasswordLeaked = (MainViewModel.Database.User.WarningsToNotify & Passkey.Interfaces.Enums.WarningType.PasswordLeakedWarning) != 0; + } + } + } +} diff --git a/GUI/WPF/Views/AccountPasswordsWarningView.xaml b/GUI/WPF/Views/AccountPasswordsWarningView.xaml new file mode 100644 index 0000000..1cb2563 --- /dev/null +++ b/GUI/WPF/Views/AccountPasswordsWarningView.xaml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + +