From 12d5b6e1d71fcb92c7139960e4ffa8fc883d382c Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sat, 11 Jan 2025 17:26:16 -0600 Subject: [PATCH 01/23] Remove 'UploadNewKeys' from LoginResponse --- .../Events/RecoveryKeyCreatedEventArgs.cs | 13 ++ .../Events/UserLoggedInEventArgs.cs | 5 +- .../Interfaces/Services/IUserKeysService.cs | 13 +- .../Services/UserKeysService.cs | 165 ++++++++++++------ .../Services/UserSessionService.cs | 25 +-- .../UserAuthentication/Login/LoginResponse.cs | 5 +- .../Keys/Queries/GetPrivateKeyQuery.cs | 3 +- .../Commands/UserLoginCommand.cs | 14 +- Crypter.Web/Shared/MainLayout.razor.cs | 42 ++--- .../Shared/Modal/RecoveryKeyModal.razor.cs | 32 ++-- 10 files changed, 167 insertions(+), 150 deletions(-) create mode 100644 Crypter.Common.Client/Events/RecoveryKeyCreatedEventArgs.cs diff --git a/Crypter.Common.Client/Events/RecoveryKeyCreatedEventArgs.cs b/Crypter.Common.Client/Events/RecoveryKeyCreatedEventArgs.cs new file mode 100644 index 000000000..77a48a65e --- /dev/null +++ b/Crypter.Common.Client/Events/RecoveryKeyCreatedEventArgs.cs @@ -0,0 +1,13 @@ +using Crypter.Common.Client.Models; + +namespace Crypter.Common.Client.Events; + +public sealed class RecoveryKeyCreatedEventArgs +{ + public RecoveryKey RecoveryKey { get; } + + public RecoveryKeyCreatedEventArgs(RecoveryKey recoveryKey) + { + RecoveryKey = recoveryKey; + } +} diff --git a/Crypter.Common.Client/Events/UserLoggedInEventArgs.cs b/Crypter.Common.Client/Events/UserLoggedInEventArgs.cs index e9e10c390..50e09c226 100644 --- a/Crypter.Common.Client/Events/UserLoggedInEventArgs.cs +++ b/Crypter.Common.Client/Events/UserLoggedInEventArgs.cs @@ -36,17 +36,14 @@ public class UserLoggedInEventArgs : EventArgs public Password Password { get; init; } public VersionedPassword VersionedPassword { get; init; } public bool RememberUser { get; init; } - public bool UploadNewKeys { get; init; } public bool ShowRecoveryKeyModal { get; init; } - public UserLoggedInEventArgs(Username username, Password password, VersionedPassword versionedPassword, - bool rememberUser, bool uploadNewKeys, bool showRecoveryKeyModal) + public UserLoggedInEventArgs(Username username, Password password, VersionedPassword versionedPassword, bool rememberUser, bool showRecoveryKeyModal) { Username = username; Password = password; VersionedPassword = versionedPassword; RememberUser = rememberUser; - UploadNewKeys = uploadNewKeys; ShowRecoveryKeyModal = showRecoveryKeyModal; } } diff --git a/Crypter.Common.Client/Interfaces/Services/IUserKeysService.cs b/Crypter.Common.Client/Interfaces/Services/IUserKeysService.cs index ff5cfd287..947c9d0cd 100644 --- a/Crypter.Common.Client/Interfaces/Services/IUserKeysService.cs +++ b/Crypter.Common.Client/Interfaces/Services/IUserKeysService.cs @@ -24,10 +24,8 @@ * Contact the current copyright holder to discuss commercial license options. */ -using System.Threading.Tasks; -using Crypter.Common.Client.Models; -using Crypter.Common.Contracts.Features.UserAuthentication; -using Crypter.Common.Primitives; +using System; +using Crypter.Common.Client.Events; using EasyMonads; namespace Crypter.Common.Client.Interfaces.Services; @@ -36,10 +34,5 @@ public interface IUserKeysService { Maybe MasterKey { get; } Maybe PrivateKey { get; } - - Task DownloadExistingKeysAsync(Username username, Password password, bool trustDevice); - Task DownloadExistingKeysAsync(byte[] credentialKey, bool trustDevice); - - Task> UploadNewKeysAsync(Username username, Password password, VersionedPassword versionedPassword, bool trustDevice); - Task> UploadNewKeysAsync(VersionedPassword versionedPassword, byte[] credentialKey, bool trustDevice); + event EventHandler RecoveryKeyCreatedEventHandler; } diff --git a/Crypter.Common.Client/Services/UserKeysService.cs b/Crypter.Common.Client/Services/UserKeysService.cs index d24fb970f..1ccb477b6 100644 --- a/Crypter.Common.Client/Services/UserKeysService.cs +++ b/Crypter.Common.Client/Services/UserKeysService.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -33,7 +33,6 @@ using Crypter.Common.Client.Models; using Crypter.Common.Contracts.Features.Keys; using Crypter.Common.Contracts.Features.UserAuthentication; -using Crypter.Common.Primitives; using Crypter.Crypto.Common; using Crypter.Crypto.Common.KeyExchange; using EasyMonads; @@ -48,12 +47,12 @@ public class UserKeysService : IUserKeysService, IDisposable private readonly IUserKeysRepository _userKeysRepository; private readonly IUserSessionService _userSessionService; + private EventHandler? _recoveryKeyCreatedEventHandler; + public Maybe MasterKey { get; private set; } = Maybe.None; public Maybe PrivateKey { get; private set; } = Maybe.None; - - public UserKeysService(ICrypterApiClient crypterApiClient, ICryptoProvider cryptoProvider, - IUserPasswordService userPasswordService, IUserKeysRepository userKeysRepository, - IUserSessionService userSessionService) + + public UserKeysService(ICrypterApiClient crypterApiClient, ICryptoProvider cryptoProvider, IUserPasswordService userPasswordService, IUserKeysRepository userKeysRepository, IUserSessionService userSessionService) { _crypterApiClient = crypterApiClient; _userPasswordService = userPasswordService; @@ -62,9 +61,10 @@ public UserKeysService(ICrypterApiClient crypterApiClient, ICryptoProvider crypt _userSessionService = userSessionService; _userSessionService.ServiceInitializedEventHandler += InitializeAsync; + _userSessionService.UserLoggedInEventHandler += HandleUserLoginAsync; _userSessionService.UserLoggedOutEventHandler += Recycle; } - + private async void InitializeAsync(object? _, UserSessionServiceInitializedEventArgs args) { if (args.IsLoggedIn) @@ -74,56 +74,76 @@ private async void InitializeAsync(object? _, UserSessionServiceInitializedEvent } } - #region Download Existing Keys - - public Task DownloadExistingKeysAsync(Username username, Password password, bool trustDevice) - { - return _userPasswordService - .DeriveUserCredentialKeyAsync(username, password, _userPasswordService.CurrentPasswordVersion) - .IfSomeAsync(credentialKey => DownloadExistingKeysAsync(credentialKey, trustDevice)); - } - - public Task DownloadExistingKeysAsync(byte[] credentialKey, bool trustDevice) + private async void HandleUserLoginAsync(object? _, UserLoggedInEventArgs args) { - return DownloadAndDecryptMasterKey(credentialKey) - .BindAsync(masterKey => DownloadAndDecryptPrivateKey(masterKey) - .IfSomeAsync(privateKey => StoreSecretKeys(masterKey, privateKey, trustDevice))); + await _userPasswordService.DeriveUserCredentialKeyAsync(args.Username, args.Password, _userPasswordService.CurrentPasswordVersion) + .BindAsync(async credentialKey => await GetOrCreateMasterKeyAsync(args.VersionedPassword, credentialKey) + .BindAsync(x => new { CredentialKey = credentialKey, x.MasterKey, x.RecoveryKey })) + .BindAsync(async carryData => await GetOrCreateKeyPairAsync(carryData.MasterKey) + .BindAsync(x => new { carryData.CredentialKey, carryData.MasterKey, x.PrivateKey, carryData.RecoveryKey })) + .IfSomeAsync(async carryData => + { + await StoreSecretKeysAsync(carryData.MasterKey, carryData.PrivateKey, args.RememberUser); + carryData.RecoveryKey.IfSome(HandleRecoveryKeyCreatedEvent); + }); } - private Task> DownloadAndDecryptMasterKey(byte[] credentialKey) + /// + /// Get the existing master key from the API and decrypt it. + /// If the master key does not already exist, create and upload a new one. + /// + /// + /// + /// + private async Task> GetOrCreateMasterKeyAsync(VersionedPassword versionedPassword, byte[] credentialKey) { - return _crypterApiClient.UserKey.GetMasterKeyAsync() - .MapAsync(x => _cryptoProvider.Encryption.Decrypt(credentialKey, x.Nonce, x.EncryptedKey)) - .ToMaybeTask(); + return await _crypterApiClient.UserKey.GetMasterKeyAsync() + .BindAsync(x => + { + byte[] decryptedMasterKey = _cryptoProvider.Encryption.Decrypt(credentialKey, x.Nonce, x.EncryptedKey); + return new GetOrCreateMasterKeyResult(decryptedMasterKey, Maybe.None); + }) + .MatchAsync( + neither: Maybe.None, + leftAsync: async x => +#pragma warning disable CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. + x switch +#pragma warning restore CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. + { + GetMasterKeyError.UnknownError => await Maybe.None.AsTask(), + GetMasterKeyError.NotFound => await UploadNewMasterKeyAsync(versionedPassword, credentialKey) + }, + right: x => x); } - private Task> DownloadAndDecryptPrivateKey(byte[] masterKey) + /// + /// Get the existing private key from the API and decrypt it. + /// If the private key does not already exist, create and upload a new key pair. + /// + /// + /// + private async Task> GetOrCreateKeyPairAsync(byte[] masterKey) { - return _crypterApiClient.UserKey.GetPrivateKeyAsync() - .MapAsync(x => _cryptoProvider.Encryption.Decrypt(masterKey, x.Nonce, x.EncryptedKey)) - .ToMaybeTask(); + return await _crypterApiClient.UserKey.GetPrivateKeyAsync() + .BindAsync(x => + { + byte[] decryptedPrivateKey = _cryptoProvider.Encryption.Decrypt(masterKey, x.Nonce, x.EncryptedKey); + return new GetOrCreateKeyPairResult(decryptedPrivateKey); + }) + .MatchAsync( + neither: Maybe.None, + leftAsync: async x => +#pragma warning disable CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. + x switch +#pragma warning restore CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. + { + GetPrivateKeyError.UnkownError => await Maybe.None.AsTask(), + GetPrivateKeyError.NotFound => await UploadNewUserKeyPairAsync(masterKey) + }, + right: x => x); } - - #endregion - - #region Upload New Keys - - public Task> UploadNewKeysAsync(Username username, Password password, VersionedPassword versionedPassword, bool trustDevice) - { - return _userPasswordService - .DeriveUserCredentialKeyAsync(username, password, _userPasswordService.CurrentPasswordVersion) - .BindAsync(credentialKey => UploadNewKeysAsync(versionedPassword, credentialKey, trustDevice)); - } - - public Task> UploadNewKeysAsync(VersionedPassword versionedPassword, byte[] credentialKey, bool trustDevice) - { - return UploadNewMasterKeyAsync(versionedPassword, credentialKey) - .BindAsync(recoveryKey => UploadNewUserKeyPairAsync(recoveryKey.MasterKey) - .IfSomeAsync(privateKey => StoreSecretKeys(recoveryKey.MasterKey, privateKey, trustDevice)) - .BindAsync(_ => recoveryKey)); - } - - private Task> UploadNewMasterKeyAsync(VersionedPassword versionedPassword, byte[] credentialKey) + + private Task> UploadNewMasterKeyAsync(VersionedPassword versionedPassword, byte[] credentialKey) { byte[] newMasterKey = _cryptoProvider.Random.GenerateRandomBytes((int)_cryptoProvider.Encryption.KeySize); byte[] nonce = _cryptoProvider.Random.GenerateRandomBytes((int)_cryptoProvider.Encryption.NonceSize); @@ -133,10 +153,10 @@ private Task> UploadNewMasterKeyAsync(VersionedPassword versi InsertMasterKeyRequest request = new InsertMasterKeyRequest(versionedPassword.Password, encryptedMasterKey, nonce, recoveryProof); return _crypterApiClient.UserKey.InsertMasterKeyAsync(request) .ToMaybeTask() - .BindAsync(_ => new RecoveryKey(newMasterKey, request.RecoveryProof)); + .BindAsync(_ => new GetOrCreateMasterKeyResult(newMasterKey, new RecoveryKey(newMasterKey, request.RecoveryProof))); } - private Task> UploadNewUserKeyPairAsync(byte[] masterKey) + private Task> UploadNewUserKeyPairAsync(byte[] masterKey) { X25519KeyPair keyPair = _cryptoProvider.KeyExchange.GenerateKeyPair(); byte[] nonce = _cryptoProvider.Random.GenerateRandomBytes((int)_cryptoProvider.Encryption.NonceSize); @@ -145,20 +165,28 @@ private Task> UploadNewUserKeyPairAsync(byte[] masterKey) InsertKeyPairRequest request = new InsertKeyPairRequest(encryptedPrivateKey, keyPair.PublicKey, nonce); return _crypterApiClient.UserKey.InsertKeyPairAsync(request) .ToMaybeTask() - .BindAsync(_ => keyPair.PrivateKey); + .BindAsync(_ => new GetOrCreateKeyPairResult(keyPair.PrivateKey)); } - - #endregion - - private async Task StoreSecretKeys(byte[] masterKey, byte[] privateKey, bool trustDevice) + + private async Task StoreSecretKeysAsync(byte[] masterKey, byte[] privateKey, bool trustDevice) { MasterKey = masterKey; PrivateKey = privateKey; await _userKeysRepository.StoreMasterKeyAsync(masterKey, trustDevice); await _userKeysRepository.StorePrivateKeyAsync(privateKey, trustDevice); - return Unit.Default; } + private void HandleRecoveryKeyCreatedEvent(RecoveryKey recoveryKey) => + _recoveryKeyCreatedEventHandler?.Invoke(this, new RecoveryKeyCreatedEventArgs(recoveryKey)); + + public event EventHandler RecoveryKeyCreatedEventHandler + { + add => _recoveryKeyCreatedEventHandler = + (EventHandler)Delegate.Combine(_recoveryKeyCreatedEventHandler, value); + remove => _recoveryKeyCreatedEventHandler = + (EventHandler?)Delegate.Remove(_recoveryKeyCreatedEventHandler, value); + } + private void Recycle(object? _, EventArgs __) { MasterKey = Maybe.None; @@ -168,7 +196,30 @@ private void Recycle(object? _, EventArgs __) public void Dispose() { _userSessionService.ServiceInitializedEventHandler -= InitializeAsync; + _userSessionService.UserLoggedInEventHandler -= HandleUserLoginAsync; _userSessionService.UserLoggedOutEventHandler -= Recycle; GC.SuppressFinalize(this); } + + private sealed record GetOrCreateMasterKeyResult + { + public byte[] MasterKey { get; } + public Maybe RecoveryKey { get; } + + public GetOrCreateMasterKeyResult(byte[] masterKey, Maybe recoveryKey) + { + MasterKey = masterKey; + RecoveryKey = recoveryKey; + } + } + + private sealed record GetOrCreateKeyPairResult + { + public byte[] PrivateKey { get; } + + public GetOrCreateKeyPairResult(byte[] privateKey) + { + PrivateKey = privateKey; + } + } } diff --git a/Crypter.Common.Client/Services/UserSessionService.cs b/Crypter.Common.Client/Services/UserSessionService.cs index 1df181d1f..7038be77e 100644 --- a/Crypter.Common.Client/Services/UserSessionService.cs +++ b/Crypter.Common.Client/Services/UserSessionService.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -140,12 +140,12 @@ public Task> LoginAsync(Username username, Password pas async versionedPassword => { List versionedPasswords = [versionedPassword]; - var loginTask = from loginResponse in LoginRecursiveAsync(username, password, versionedPasswords, _trustDeviceRefreshTokenTypeMap[rememberUser]) + Task> loginTask = from loginResponse in LoginRecursiveAsync(username, password, versionedPasswords, _trustDeviceRefreshTokenTypeMap[rememberUser]) from unit0 in Either.FromRightAsync(StoreSessionInfo(loginResponse, rememberUser)) select loginResponse; Either loginResult = await loginTask; - loginResult.DoRight(x => HandleUserLoggedInEvent(username, password, versionedPassword, rememberUser, x.UploadNewKeys, x.ShowRecoveryKey)); + loginResult.DoRight(x => HandleUserLoggedInEvent(username, password, versionedPassword, rememberUser, x.ShowRecoveryKey)); return loginResult.Map(_ => Unit.Default); }); } @@ -169,10 +169,8 @@ private Task> LoginRecursiveAsync(Username use return await LoginRecursiveAsync(username, password, versionedPasswords, refreshTokenType); }); } - else - { - return error; - } + + return error; }, response => response, LoginError.UnknownError); @@ -203,11 +201,8 @@ private async void OnRefreshTokenRejectedByApi(object? _, EventArgs __) => private void HandleServiceInitializedEvent() => _serviceInitializedEventHandler?.Invoke(this, new UserSessionServiceInitializedEventArgs(Session.IsSome)); - private void HandleUserLoggedInEvent(Username username, Password password, VersionedPassword versionedPassword, - bool rememberUser, bool uploadNewKeys, bool showRecoveryKeyModal) => - _userLoggedInEventHandler?.Invoke(this, - new UserLoggedInEventArgs(username, password, versionedPassword, rememberUser, uploadNewKeys, - showRecoveryKeyModal)); + private void HandleUserLoggedInEvent(Username username, Password password, VersionedPassword versionedPassword, bool rememberUser, bool showRecoveryKeyModal) => + _userLoggedInEventHandler?.Invoke(this, new UserLoggedInEventArgs(username, password, versionedPassword, rememberUser, showRecoveryKeyModal)); private void HandleUserLoggedOutEvent() => _userLoggedOutEventHandler?.Invoke(this, EventArgs.Empty); @@ -218,11 +213,9 @@ private void HandleTestPasswordSuccessEvent(Username username, Password password public event EventHandler ServiceInitializedEventHandler { add => _serviceInitializedEventHandler = - (EventHandler)Delegate.Combine(_serviceInitializedEventHandler, - value); + (EventHandler)Delegate.Combine(_serviceInitializedEventHandler, value); remove => _serviceInitializedEventHandler = - (EventHandler?)Delegate.Remove(_serviceInitializedEventHandler, - value); + (EventHandler?)Delegate.Remove(_serviceInitializedEventHandler, value); } public event EventHandler UserLoggedInEventHandler diff --git a/Crypter.Common/Contracts/Features/UserAuthentication/Login/LoginResponse.cs b/Crypter.Common/Contracts/Features/UserAuthentication/Login/LoginResponse.cs index 8019192a3..b9de26cd6 100644 --- a/Crypter.Common/Contracts/Features/UserAuthentication/Login/LoginResponse.cs +++ b/Crypter.Common/Contracts/Features/UserAuthentication/Login/LoginResponse.cs @@ -33,17 +33,14 @@ public class LoginResponse public string Username { get; init; } public string AuthenticationToken { get; init; } public string RefreshToken { get; init; } - public bool UploadNewKeys { get; init; } public bool ShowRecoveryKey { get; init; } [JsonConstructor] - public LoginResponse(string username, string authenticationToken, string refreshToken, bool uploadNewKeys, - bool showRecoveryKey) + public LoginResponse(string username, string authenticationToken, string refreshToken, bool showRecoveryKey) { Username = username; AuthenticationToken = authenticationToken; RefreshToken = refreshToken; - UploadNewKeys = uploadNewKeys; ShowRecoveryKey = showRecoveryKey; } } diff --git a/Crypter.Core/Features/Keys/Queries/GetPrivateKeyQuery.cs b/Crypter.Core/Features/Keys/Queries/GetPrivateKeyQuery.cs index 479e7f34d..b8b95c5a1 100644 --- a/Crypter.Core/Features/Keys/Queries/GetPrivateKeyQuery.cs +++ b/Crypter.Core/Features/Keys/Queries/GetPrivateKeyQuery.cs @@ -48,8 +48,7 @@ public GetPrivateKeyQueryHandler(DataContext dataContext) _dataContext = dataContext; } - public Task> Handle(GetPrivateKeyQuery request, - CancellationToken cancellationToken) + public Task> Handle(GetPrivateKeyQuery request, CancellationToken cancellationToken) { return Either.FromRightNullableAsync( _dataContext.UserKeyPairs diff --git a/Crypter.Core/Features/UserAuthentication/Commands/UserLoginCommand.cs b/Crypter.Core/Features/UserAuthentication/Commands/UserLoginCommand.cs index 752d3eed9..8e3bd6682 100644 --- a/Crypter.Core/Features/UserAuthentication/Commands/UserLoginCommand.cs +++ b/Crypter.Core/Features/UserAuthentication/Commands/UserLoginCommand.cs @@ -89,9 +89,7 @@ public async Task> Handle(UserLoginCommand req .BindAsync(async validLoginRequest => await ( from foundUser in GetUserAsync(validLoginRequest) from passwordVerificationSuccess in VerifyAndUpgradePassword(validLoginRequest, foundUser).AsTask() - from loginResponse in Either.FromRightAsync( - CreateLoginResponseAsync(foundUser, validLoginRequest.RefreshTokenType, - request.DeviceDescription)) + from loginResponse in CreateLoginResponseAsync(foundUser, validLoginRequest.RefreshTokenType, request.DeviceDescription) select loginResponse) ) .DoRightAsync(async _ => @@ -205,7 +203,7 @@ private Either VerifyAndUpgradePassword(ValidLoginRequest vali return Unit.Default; } - private async Task CreateLoginResponseAsync(UserEntity userEntity, TokenType refreshTokenType, string deviceDescription) + private async Task> CreateLoginResponseAsync(UserEntity userEntity, TokenType refreshTokenType, string deviceDescription) { userEntity.LastLogin = DateTime.UtcNow; @@ -224,12 +222,10 @@ private async Task CreateLoginResponseAsync(UserEntity userEntity string authToken = _tokenService.NewAuthenticationToken(userEntity.Id); await Common.PublishRefreshTokenCreatedEventAsync(_publisher, refreshToken); - bool userHasConsentedToRecoveryKeyRisks = - userEntity.Consents!.Any(x => x.ConsentType == ConsentType.RecoveryKeyRisks); - bool userNeedsNewKeys = userEntity.MasterKey is null && userEntity.KeyPair is null; + bool userHasConsentedToRecoveryKeyRisks = userEntity.Consents! + .Any(x => x.ConsentType == ConsentType.RecoveryKeyRisks); - return new LoginResponse(userEntity.Username, authToken, refreshToken.Token, userNeedsNewKeys, - !userHasConsentedToRecoveryKeyRisks); + return new LoginResponse(userEntity.Username, authToken, refreshToken.Token, !userHasConsentedToRecoveryKeyRisks); } private Either> GetValidClientPasswords(List clientPasswords) diff --git a/Crypter.Web/Shared/MainLayout.razor.cs b/Crypter.Web/Shared/MainLayout.razor.cs index 5c719e9c7..d67a7a609 100644 --- a/Crypter.Web/Shared/MainLayout.razor.cs +++ b/Crypter.Web/Shared/MainLayout.razor.cs @@ -64,43 +64,30 @@ public class MainLayoutBase : LayoutComponentBase, IDisposable protected override async Task OnInitializedAsync() { UserSessionService.UserLoggedInEventHandler += HandleUserLoggedInEvent; + UserKeysService.RecoveryKeyCreatedEventHandler += HandleRecoveryKeyCreatedEvent; UserPasswordService.PasswordHashBeginEventHandler += ShowPasswordHashingModal; UserPasswordService.PasswordHashEndEventHandler += ClosePasswordHashingModal; await BrowserRepository.InitializeAsync(); - await Task.WhenAll(new Task[] - { - BlazorSodiumService.InitializeAsync(), - FileSaverService.InitializeAsync(), - BrowserFunctions.InitializeAsync() - }); + await Task.WhenAll(BlazorSodiumService.InitializeAsync(), FileSaverService.InitializeAsync(), BrowserFunctions.InitializeAsync()); ServicesInitialized = true; } - private async void HandleUserLoggedInEvent(object? sender, UserLoggedInEventArgs args) + private async void HandleUserLoggedInEvent(object? _, UserLoggedInEventArgs args) { - await UserPasswordService - .DeriveUserCredentialKeyAsync(args.Username, args.Password, UserPasswordService.CurrentPasswordVersion) - .IfSomeAsync(async credentialKey => - { - if (args.UploadNewKeys) - { - await UserKeysService.UploadNewKeysAsync(args.VersionedPassword, credentialKey, args.RememberUser); - } - else - { - await UserKeysService.DownloadExistingKeysAsync(credentialKey, args.RememberUser); - } - - if (args.ShowRecoveryKeyModal) - { - await RecoveryKeyModal.OpenAsync(args.VersionedPassword); - } - }); + if (args.ShowRecoveryKeyModal) + { + await RecoveryKeyModal.OpenAsync(args.VersionedPassword); + } } - private void ShowPasswordHashingModal(object? sender, PasswordHashBeginEventArgs args) + private void HandleRecoveryKeyCreatedEvent(object? _, RecoveryKeyCreatedEventArgs args) + { + RecoveryKeyModal.Open(args.RecoveryKey); + } + + private void ShowPasswordHashingModal(object? _, PasswordHashBeginEventArgs args) { switch (args.HashType) { @@ -118,7 +105,7 @@ private void ShowPasswordHashingModal(object? sender, PasswordHashBeginEventArgs } } - private async void ClosePasswordHashingModal(object? sender, PasswordHashEndEventArgs args) + private async void ClosePasswordHashingModal(object? _, PasswordHashEndEventArgs args) { await SpinnerModal.CloseAsync(); } @@ -126,6 +113,7 @@ private async void ClosePasswordHashingModal(object? sender, PasswordHashEndEven public void Dispose() { UserSessionService.UserLoggedInEventHandler -= HandleUserLoggedInEvent; + UserKeysService.RecoveryKeyCreatedEventHandler -= HandleRecoveryKeyCreatedEvent; UserPasswordService.PasswordHashBeginEventHandler -= ShowPasswordHashingModal; UserPasswordService.PasswordHashEndEventHandler -= ClosePasswordHashingModal; GC.SuppressFinalize(this); diff --git a/Crypter.Web/Shared/Modal/RecoveryKeyModal.razor.cs b/Crypter.Web/Shared/Modal/RecoveryKeyModal.razor.cs index 987d51c95..46094f573 100644 --- a/Crypter.Web/Shared/Modal/RecoveryKeyModal.razor.cs +++ b/Crypter.Web/Shared/Modal/RecoveryKeyModal.razor.cs @@ -24,13 +24,11 @@ * Contact the current copyright holder to discuss commercial license options. */ - -using System; using System.Threading.Tasks; using Crypter.Common.Client.Interfaces.HttpClients; using Crypter.Common.Client.Interfaces.Services; +using Crypter.Common.Client.Models; using Crypter.Common.Contracts.Features.UserAuthentication; -using Crypter.Common.Primitives; using Crypter.Web.Shared.Modal.Template; using EasyMonads; using Microsoft.AspNetCore.Components; @@ -52,34 +50,26 @@ public partial class RecoveryKeyModal private ModalBehavior _modalBehaviorRef = null!; - public async Task OpenAsync(Username username, Password password) + public async Task OpenAsync(VersionedPassword versionedPassword) { - _recoveryKey = await UserKeysService.MasterKey - .BindAsync(async masterKey => - await UserRecoveryService.DeriveRecoveryKeyAsync(masterKey, username, password)) - .MatchAsync( - () => "An error occurred", - x => x.ToBase64String()); + Maybe recoveryKey = await UserKeysService.MasterKey + .BindAsync(async masterKey => await UserRecoveryService.DeriveRecoveryKeyAsync(masterKey, versionedPassword)); - _modalBehaviorRef.Open(); + Open(recoveryKey); } - public async Task OpenAsync(VersionedPassword versionedPassword) + public void Open(Maybe recoveryKey) { - _recoveryKey = await UserKeysService.MasterKey - .BindAsync(async masterKey => - await UserRecoveryService.DeriveRecoveryKeyAsync(masterKey, versionedPassword)) - .MatchAsync( - () => "An error occurred", - x => x.ToBase64String()); - + _recoveryKey = recoveryKey + .Match( + none: () => "An error occurred", + some: x => x.ToBase64String()); _modalBehaviorRef.Open(); } private async Task CopyRecoveryKeyToClipboardAsync() { - await JsRuntime.InvokeVoidAsync("Crypter.CopyToClipboard", - new object[] { _recoveryKey, "recoveryKeyModalCopyTooltip" }); + await JsRuntime.InvokeVoidAsync("Crypter.CopyToClipboard", _recoveryKey, "recoveryKeyModalCopyTooltip"); } private async Task OnAcknowledgedClickedAsync() From c9dd71a683336c02cc03969c77bc092388c170fa Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sat, 11 Jan 2025 20:22:47 -0600 Subject: [PATCH 02/23] Rename property --- Crypter.Common.Client/Services/UserKeysService.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Crypter.Common.Client/Services/UserKeysService.cs b/Crypter.Common.Client/Services/UserKeysService.cs index 1ccb477b6..f22339c6b 100644 --- a/Crypter.Common.Client/Services/UserKeysService.cs +++ b/Crypter.Common.Client/Services/UserKeysService.cs @@ -78,13 +78,13 @@ private async void HandleUserLoginAsync(object? _, UserLoggedInEventArgs args) { await _userPasswordService.DeriveUserCredentialKeyAsync(args.Username, args.Password, _userPasswordService.CurrentPasswordVersion) .BindAsync(async credentialKey => await GetOrCreateMasterKeyAsync(args.VersionedPassword, credentialKey) - .BindAsync(x => new { CredentialKey = credentialKey, x.MasterKey, x.RecoveryKey })) + .BindAsync(x => new { CredentialKey = credentialKey, x.MasterKey, x.NewRecoveryKey })) .BindAsync(async carryData => await GetOrCreateKeyPairAsync(carryData.MasterKey) - .BindAsync(x => new { carryData.CredentialKey, carryData.MasterKey, x.PrivateKey, carryData.RecoveryKey })) + .BindAsync(x => new { carryData.CredentialKey, carryData.MasterKey, x.PrivateKey, carryData.NewRecoveryKey })) .IfSomeAsync(async carryData => { await StoreSecretKeysAsync(carryData.MasterKey, carryData.PrivateKey, args.RememberUser); - carryData.RecoveryKey.IfSome(HandleRecoveryKeyCreatedEvent); + carryData.NewRecoveryKey.IfSome(HandleRecoveryKeyCreatedEvent); }); } @@ -204,12 +204,12 @@ public void Dispose() private sealed record GetOrCreateMasterKeyResult { public byte[] MasterKey { get; } - public Maybe RecoveryKey { get; } + public Maybe NewRecoveryKey { get; } - public GetOrCreateMasterKeyResult(byte[] masterKey, Maybe recoveryKey) + public GetOrCreateMasterKeyResult(byte[] masterKey, Maybe newRecoveryKey) { MasterKey = masterKey; - RecoveryKey = recoveryKey; + NewRecoveryKey = newRecoveryKey; } } From dafdd527590e7aa403f561415c3b375d747cc574 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sat, 11 Jan 2025 20:23:57 -0600 Subject: [PATCH 03/23] Add copyright header to source file --- .../Events/RecoveryKeyCreatedEventArgs.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Crypter.Common.Client/Events/RecoveryKeyCreatedEventArgs.cs b/Crypter.Common.Client/Events/RecoveryKeyCreatedEventArgs.cs index 77a48a65e..2034cacaf 100644 --- a/Crypter.Common.Client/Events/RecoveryKeyCreatedEventArgs.cs +++ b/Crypter.Common.Client/Events/RecoveryKeyCreatedEventArgs.cs @@ -1,3 +1,29 @@ +/* + * Copyright (C) 2025 Crypter File Transfer + * + * This file is part of the Crypter file transfer project. + * + * Crypter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Crypter source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the aforementioned license + * by purchasing a commercial license. Buying such a license is mandatory + * as soon as you develop commercial activities involving the Crypter source + * code without disclosing the source code of your own applications. + * + * Contact the current copyright holder to discuss commercial license options. + */ + using Crypter.Common.Client.Models; namespace Crypter.Common.Client.Events; From af6beedbf1c8dfadcf18c9f75522f8b249051bf8 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sat, 11 Jan 2025 22:31:30 -0600 Subject: [PATCH 04/23] Handle concurrent entries to ShowRecoveryKeyModal --- .../Shared/Modal/RecoveryKeyModal.razor.cs | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/Crypter.Web/Shared/Modal/RecoveryKeyModal.razor.cs b/Crypter.Web/Shared/Modal/RecoveryKeyModal.razor.cs index 46094f573..0633ecad2 100644 --- a/Crypter.Web/Shared/Modal/RecoveryKeyModal.razor.cs +++ b/Crypter.Web/Shared/Modal/RecoveryKeyModal.razor.cs @@ -24,6 +24,7 @@ * Contact the current copyright holder to discuss commercial license options. */ +using System; using System.Threading.Tasks; using Crypter.Common.Client.Interfaces.HttpClients; using Crypter.Common.Client.Interfaces.Services; @@ -49,22 +50,37 @@ public partial class RecoveryKeyModal private string _recoveryKey = string.Empty; private ModalBehavior _modalBehaviorRef = null!; + + private bool _isOpen = false; public async Task OpenAsync(VersionedPassword versionedPassword) { - Maybe recoveryKey = await UserKeysService.MasterKey - .BindAsync(async masterKey => await UserRecoveryService.DeriveRecoveryKeyAsync(masterKey, versionedPassword)); - - Open(recoveryKey); + if (!_isOpen) + { + _isOpen = true; + Maybe recoveryKey = await UserKeysService.MasterKey + .BindAsync(async masterKey => await UserRecoveryService.DeriveRecoveryKeyAsync(masterKey, versionedPassword)); + _recoveryKey = RecoveryKeyToString(recoveryKey); + _modalBehaviorRef.Open(); + } } public void Open(Maybe recoveryKey) { - _recoveryKey = recoveryKey + if (!_isOpen) + { + _isOpen = true; + _recoveryKey = RecoveryKeyToString(recoveryKey); + _modalBehaviorRef.Open(); + } + } + + private string RecoveryKeyToString(Maybe recoveryKey) + { + return recoveryKey .Match( none: () => "An error occurred", some: x => x.ToBase64String()); - _modalBehaviorRef.Open(); } private async Task CopyRecoveryKeyToClipboardAsync() @@ -76,5 +92,6 @@ private async Task OnAcknowledgedClickedAsync() { await CrypterApiService.UserConsent.ConsentToRecoveryKeyRisksAsync(); _modalBehaviorRef.Close(); + _isOpen = false; } } From 5451b63f629d4d72d08c07299fe6681abd3b7c06 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sat, 11 Jan 2025 23:26:41 -0600 Subject: [PATCH 05/23] Only an event sent from UserKeysService will open the RecoveryKeyModal --- ...entArgs.cs => EmitRecoveryKeyEventArgs.cs} | 4 +-- .../Interfaces/Services/IUserKeysService.cs | 2 +- .../Services/UserKeysService.cs | 34 +++++++++++++------ .../Services/UserRecoveryService.cs | 6 ++-- .../SubmitRecovery_Tests.cs | 31 ++++++----------- Crypter.Web/Shared/MainLayout.razor.cs | 16 ++------- .../Shared/Modal/RecoveryKeyModal.razor.cs | 34 +++---------------- .../UserSettings/UserSettingsKeys.razor.cs | 5 ++- 8 files changed, 47 insertions(+), 85 deletions(-) rename Crypter.Common.Client/Events/{RecoveryKeyCreatedEventArgs.cs => EmitRecoveryKeyEventArgs.cs} (92%) diff --git a/Crypter.Common.Client/Events/RecoveryKeyCreatedEventArgs.cs b/Crypter.Common.Client/Events/EmitRecoveryKeyEventArgs.cs similarity index 92% rename from Crypter.Common.Client/Events/RecoveryKeyCreatedEventArgs.cs rename to Crypter.Common.Client/Events/EmitRecoveryKeyEventArgs.cs index 2034cacaf..596f193f1 100644 --- a/Crypter.Common.Client/Events/RecoveryKeyCreatedEventArgs.cs +++ b/Crypter.Common.Client/Events/EmitRecoveryKeyEventArgs.cs @@ -28,11 +28,11 @@ namespace Crypter.Common.Client.Events; -public sealed class RecoveryKeyCreatedEventArgs +public sealed class EmitRecoveryKeyEventArgs { public RecoveryKey RecoveryKey { get; } - public RecoveryKeyCreatedEventArgs(RecoveryKey recoveryKey) + public EmitRecoveryKeyEventArgs(RecoveryKey recoveryKey) { RecoveryKey = recoveryKey; } diff --git a/Crypter.Common.Client/Interfaces/Services/IUserKeysService.cs b/Crypter.Common.Client/Interfaces/Services/IUserKeysService.cs index 947c9d0cd..737920344 100644 --- a/Crypter.Common.Client/Interfaces/Services/IUserKeysService.cs +++ b/Crypter.Common.Client/Interfaces/Services/IUserKeysService.cs @@ -34,5 +34,5 @@ public interface IUserKeysService { Maybe MasterKey { get; } Maybe PrivateKey { get; } - event EventHandler RecoveryKeyCreatedEventHandler; + event EventHandler EmitRecoveryKeyEventHandler; } diff --git a/Crypter.Common.Client/Services/UserKeysService.cs b/Crypter.Common.Client/Services/UserKeysService.cs index f22339c6b..40838bac3 100644 --- a/Crypter.Common.Client/Services/UserKeysService.cs +++ b/Crypter.Common.Client/Services/UserKeysService.cs @@ -33,6 +33,7 @@ using Crypter.Common.Client.Models; using Crypter.Common.Contracts.Features.Keys; using Crypter.Common.Contracts.Features.UserAuthentication; +using Crypter.Common.Primitives; using Crypter.Crypto.Common; using Crypter.Crypto.Common.KeyExchange; using EasyMonads; @@ -46,19 +47,21 @@ public class UserKeysService : IUserKeysService, IDisposable private readonly IUserPasswordService _userPasswordService; private readonly IUserKeysRepository _userKeysRepository; private readonly IUserSessionService _userSessionService; + private readonly IUserRecoveryService _userRecoveryService; - private EventHandler? _recoveryKeyCreatedEventHandler; + private EventHandler? _emitRecoveryKeyEventHandler; public Maybe MasterKey { get; private set; } = Maybe.None; public Maybe PrivateKey { get; private set; } = Maybe.None; - public UserKeysService(ICrypterApiClient crypterApiClient, ICryptoProvider cryptoProvider, IUserPasswordService userPasswordService, IUserKeysRepository userKeysRepository, IUserSessionService userSessionService) + public UserKeysService(ICrypterApiClient crypterApiClient, ICryptoProvider cryptoProvider, IUserPasswordService userPasswordService, IUserKeysRepository userKeysRepository, IUserSessionService userSessionService, IUserRecoveryService userRecoveryService) { _crypterApiClient = crypterApiClient; _userPasswordService = userPasswordService; _cryptoProvider = cryptoProvider; _userKeysRepository = userKeysRepository; _userSessionService = userSessionService; + _userRecoveryService = userRecoveryService; _userSessionService.ServiceInitializedEventHandler += InitializeAsync; _userSessionService.UserLoggedInEventHandler += HandleUserLoginAsync; @@ -84,10 +87,19 @@ await _userPasswordService.DeriveUserCredentialKeyAsync(args.Username, args.Pass .IfSomeAsync(async carryData => { await StoreSecretKeysAsync(carryData.MasterKey, carryData.PrivateKey, args.RememberUser); - carryData.NewRecoveryKey.IfSome(HandleRecoveryKeyCreatedEvent); + await carryData.NewRecoveryKey + .IfSome(HandleEmitRecoveryKeyEvent) + .IfNoneAsync(async () => + { + if (args.ShowRecoveryKeyModal) + { + await _userRecoveryService.DeriveRecoveryKeyAsync(carryData.MasterKey, args.VersionedPassword) + .IfSomeAsync(HandleEmitRecoveryKeyEvent); + } + }); }); } - + /// /// Get the existing master key from the API and decrypt it. /// If the master key does not already exist, create and upload a new one. @@ -176,15 +188,15 @@ private async Task StoreSecretKeysAsync(byte[] masterKey, byte[] privateKey, boo await _userKeysRepository.StorePrivateKeyAsync(privateKey, trustDevice); } - private void HandleRecoveryKeyCreatedEvent(RecoveryKey recoveryKey) => - _recoveryKeyCreatedEventHandler?.Invoke(this, new RecoveryKeyCreatedEventArgs(recoveryKey)); + private void HandleEmitRecoveryKeyEvent(RecoveryKey recoveryKey) => + _emitRecoveryKeyEventHandler?.Invoke(this, new EmitRecoveryKeyEventArgs(recoveryKey)); - public event EventHandler RecoveryKeyCreatedEventHandler + public event EventHandler EmitRecoveryKeyEventHandler { - add => _recoveryKeyCreatedEventHandler = - (EventHandler)Delegate.Combine(_recoveryKeyCreatedEventHandler, value); - remove => _recoveryKeyCreatedEventHandler = - (EventHandler?)Delegate.Remove(_recoveryKeyCreatedEventHandler, value); + add => _emitRecoveryKeyEventHandler = + (EventHandler)Delegate.Combine(_emitRecoveryKeyEventHandler, value); + remove => _emitRecoveryKeyEventHandler = + (EventHandler?)Delegate.Remove(_emitRecoveryKeyEventHandler, value); } private void Recycle(object? _, EventArgs __) diff --git a/Crypter.Common.Client/Services/UserRecoveryService.cs b/Crypter.Common.Client/Services/UserRecoveryService.cs index 6d80fed19..beb05072a 100644 --- a/Crypter.Common.Client/Services/UserRecoveryService.cs +++ b/Crypter.Common.Client/Services/UserRecoveryService.cs @@ -43,8 +43,7 @@ public class UserRecoveryService : IUserRecoveryService private readonly ICryptoProvider _cryptoProvider; private readonly IUserPasswordService _userPasswordService; - public UserRecoveryService(ICrypterApiClient crypterApiClient, ICryptoProvider cryptoProvider, - IUserPasswordService userPasswordService) + public UserRecoveryService(ICrypterApiClient crypterApiClient, ICryptoProvider cryptoProvider, IUserPasswordService userPasswordService) { _crypterApiClient = crypterApiClient; _cryptoProvider = cryptoProvider; @@ -62,8 +61,7 @@ public async Task>> Submit Either passwordDerivatives = await _userPasswordService .DeriveUserAuthenticationPasswordAsync(username, newPassword, _userPasswordService.CurrentPasswordVersion) .ToEitherAsync(SubmitAccountRecoveryError.PasswordHashFailure) - .MapAsync(async versionedPassword => await _userPasswordService.DeriveUserCredentialKeyAsync(username, - newPassword, _userPasswordService.CurrentPasswordVersion) + .MapAsync(async versionedPassword => await _userPasswordService.DeriveUserCredentialKeyAsync(username, newPassword, _userPasswordService.CurrentPasswordVersion) .ToEitherAsync(SubmitAccountRecoveryError.PasswordHashFailure) .BindAsync(credentialKey => new PasswordDerivatives { diff --git a/Crypter.Test/Integration_Tests/UserRecovery_Tests/SubmitRecovery_Tests.cs b/Crypter.Test/Integration_Tests/UserRecovery_Tests/SubmitRecovery_Tests.cs index 5d99dae57..b9531952c 100644 --- a/Crypter.Test/Integration_Tests/UserRecovery_Tests/SubmitRecovery_Tests.cs +++ b/Crypter.Test/Integration_Tests/UserRecovery_Tests/SubmitRecovery_Tests.cs @@ -94,10 +94,8 @@ public async Task TeardownTestAsync() [TestCase(true)] public async Task Submit_Recovery_Works_Async(bool withRecoveryProof) { - RegistrationRequest registrationRequest = TestData.GetRegistrationRequest(TestData.DefaultUsername, - TestData.DefaultPassword, TestData.DefaultEmailAdress); - Either registrationResult = - await _client!.UserAuthentication.RegisterAsync(registrationRequest); + RegistrationRequest registrationRequest = TestData.GetRegistrationRequest(TestData.DefaultUsername, TestData.DefaultPassword, TestData.DefaultEmailAdress); + Either registrationResult = await _client!.UserAuthentication.RegisterAsync(registrationRequest); // Allow the background service to "send" the verification email and save the email verification data await Task.Delay(5000); @@ -105,8 +103,7 @@ public async Task Submit_Recovery_Works_Async(bool withRecoveryProof) Maybe recoveryKey = Maybe.None; if (withRecoveryProof) { - LoginRequest loginRequest = - TestData.GetLoginRequest(TestData.DefaultUsername, TestData.DefaultPassword); + LoginRequest loginRequest = TestData.GetLoginRequest(TestData.DefaultUsername, TestData.DefaultPassword); Either loginResult = await _client!.UserAuthentication.LoginAsync(loginRequest); await loginResult.DoRightAsync(async loginResponse => @@ -115,14 +112,11 @@ await loginResult.DoRightAsync(async loginResponse => await _clientTokenRepository!.StoreRefreshTokenAsync(loginResponse.RefreshToken, TokenType.Session); }); - (byte[] masterKey, InsertMasterKeyRequest insertMasterKeyRequest) = - TestData.GetInsertMasterKeyRequest(TestData.DefaultPassword); - Either _ = - await _client!.UserKey.InsertMasterKeyAsync(insertMasterKeyRequest); + (byte[] masterKey, InsertMasterKeyRequest insertMasterKeyRequest) = TestData.GetInsertMasterKeyRequest(TestData.DefaultPassword); + Either _ = await _client!.UserKey.InsertMasterKeyAsync(insertMasterKeyRequest); UserPasswordService userPasswordService = new UserPasswordService(_cryptoProvider!); - UserRecoveryService userRecoveryService = - new UserRecoveryService(_client, new DefaultCryptoProvider(), userPasswordService); + UserRecoveryService userRecoveryService = new UserRecoveryService(_client, new DefaultCryptoProvider(), userPasswordService); recoveryKey = await userRecoveryService.DeriveRecoveryKeyAsync(masterKey, registrationRequest.VersionedPassword); recoveryKey.IfNone(Assert.Fail); } @@ -134,19 +128,14 @@ await loginResult.DoRightAsync(async loginResponse => .FirstAsync(); string encodedVerificationCode = UrlSafeEncoder.EncodeGuidUrlSafe(verificationData.Code); - byte[] signedVerificationCode = - _cryptoProvider!.DigitalSignature.GenerateSignature(_knownKeyPair!.PrivateKey, - verificationData.Code.ToByteArray()); + byte[] signedVerificationCode = _cryptoProvider!.DigitalSignature.GenerateSignature(_knownKeyPair!.PrivateKey, verificationData.Code.ToByteArray()); string encodedVerificationSignature = UrlSafeEncoder.EncodeBytesUrlSafe(signedVerificationCode); - VerifyEmailAddressRequest verificationRequest = - new VerifyEmailAddressRequest(encodedVerificationCode, encodedVerificationSignature); - Either verificationResult = - await _client!.UserSetting.VerifyUserEmailAddressAsync(verificationRequest); + VerifyEmailAddressRequest verificationRequest = new VerifyEmailAddressRequest(encodedVerificationCode, encodedVerificationSignature); + Either verificationResult = await _client!.UserSetting.VerifyUserEmailAddressAsync(verificationRequest); EmailAddress emailAddress = EmailAddress.From(TestData.DefaultEmailAdress); - Either sendRecoveryEmailResult = - await _client!.UserRecovery.SendRecoveryEmailAsync(emailAddress); + Either sendRecoveryEmailResult = await _client!.UserRecovery.SendRecoveryEmailAsync(emailAddress); // Allow the background service to "send" the recovery email and save the recovery data await Task.Delay(5000); diff --git a/Crypter.Web/Shared/MainLayout.razor.cs b/Crypter.Web/Shared/MainLayout.razor.cs index d67a7a609..606e7766c 100644 --- a/Crypter.Web/Shared/MainLayout.razor.cs +++ b/Crypter.Web/Shared/MainLayout.razor.cs @@ -63,8 +63,7 @@ public class MainLayoutBase : LayoutComponentBase, IDisposable protected override async Task OnInitializedAsync() { - UserSessionService.UserLoggedInEventHandler += HandleUserLoggedInEvent; - UserKeysService.RecoveryKeyCreatedEventHandler += HandleRecoveryKeyCreatedEvent; + UserKeysService.EmitRecoveryKeyEventHandler += HandleRecoveryKeyCreatedEvent; UserPasswordService.PasswordHashBeginEventHandler += ShowPasswordHashingModal; UserPasswordService.PasswordHashEndEventHandler += ClosePasswordHashingModal; await BrowserRepository.InitializeAsync(); @@ -74,15 +73,7 @@ protected override async Task OnInitializedAsync() ServicesInitialized = true; } - private async void HandleUserLoggedInEvent(object? _, UserLoggedInEventArgs args) - { - if (args.ShowRecoveryKeyModal) - { - await RecoveryKeyModal.OpenAsync(args.VersionedPassword); - } - } - - private void HandleRecoveryKeyCreatedEvent(object? _, RecoveryKeyCreatedEventArgs args) + private void HandleRecoveryKeyCreatedEvent(object? _, EmitRecoveryKeyEventArgs args) { RecoveryKeyModal.Open(args.RecoveryKey); } @@ -112,8 +103,7 @@ private async void ClosePasswordHashingModal(object? _, PasswordHashEndEventArgs public void Dispose() { - UserSessionService.UserLoggedInEventHandler -= HandleUserLoggedInEvent; - UserKeysService.RecoveryKeyCreatedEventHandler -= HandleRecoveryKeyCreatedEvent; + UserKeysService.EmitRecoveryKeyEventHandler -= HandleRecoveryKeyCreatedEvent; UserPasswordService.PasswordHashBeginEventHandler -= ShowPasswordHashingModal; UserPasswordService.PasswordHashEndEventHandler -= ClosePasswordHashingModal; GC.SuppressFinalize(this); diff --git a/Crypter.Web/Shared/Modal/RecoveryKeyModal.razor.cs b/Crypter.Web/Shared/Modal/RecoveryKeyModal.razor.cs index 0633ecad2..f60e1e1c7 100644 --- a/Crypter.Web/Shared/Modal/RecoveryKeyModal.razor.cs +++ b/Crypter.Web/Shared/Modal/RecoveryKeyModal.razor.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -24,12 +24,10 @@ * Contact the current copyright holder to discuss commercial license options. */ -using System; using System.Threading.Tasks; using Crypter.Common.Client.Interfaces.HttpClients; using Crypter.Common.Client.Interfaces.Services; using Crypter.Common.Client.Models; -using Crypter.Common.Contracts.Features.UserAuthentication; using Crypter.Web.Shared.Modal.Template; using EasyMonads; using Microsoft.AspNetCore.Components; @@ -50,39 +48,16 @@ public partial class RecoveryKeyModal private string _recoveryKey = string.Empty; private ModalBehavior _modalBehaviorRef = null!; - - private bool _isOpen = false; - - public async Task OpenAsync(VersionedPassword versionedPassword) - { - if (!_isOpen) - { - _isOpen = true; - Maybe recoveryKey = await UserKeysService.MasterKey - .BindAsync(async masterKey => await UserRecoveryService.DeriveRecoveryKeyAsync(masterKey, versionedPassword)); - _recoveryKey = RecoveryKeyToString(recoveryKey); - _modalBehaviorRef.Open(); - } - } public void Open(Maybe recoveryKey) { - if (!_isOpen) - { - _isOpen = true; - _recoveryKey = RecoveryKeyToString(recoveryKey); - _modalBehaviorRef.Open(); - } - } - - private string RecoveryKeyToString(Maybe recoveryKey) - { - return recoveryKey + _recoveryKey = recoveryKey .Match( none: () => "An error occurred", some: x => x.ToBase64String()); + _modalBehaviorRef.Open(); } - + private async Task CopyRecoveryKeyToClipboardAsync() { await JsRuntime.InvokeVoidAsync("Crypter.CopyToClipboard", _recoveryKey, "recoveryKeyModalCopyTooltip"); @@ -92,6 +67,5 @@ private async Task OnAcknowledgedClickedAsync() { await CrypterApiService.UserConsent.ConsentToRecoveryKeyRisksAsync(); _modalBehaviorRef.Close(); - _isOpen = false; } } diff --git a/Crypter.Web/Shared/UserSettings/UserSettingsKeys.razor.cs b/Crypter.Web/Shared/UserSettings/UserSettingsKeys.razor.cs index 6046194c5..c0d8facec 100644 --- a/Crypter.Web/Shared/UserSettings/UserSettingsKeys.razor.cs +++ b/Crypter.Web/Shared/UserSettings/UserSettingsKeys.razor.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -62,8 +62,7 @@ protected override void OnInitialized() private async void OnPasswordTestSuccess(object? sender, UserPasswordTestSuccessEventArgs args) { _recoveryKey = await UserKeysService.MasterKey - .BindAsync(async masterKey => - await UserRecoveryService.DeriveRecoveryKeyAsync(masterKey, args.Username, args.Password)) + .BindAsync(async masterKey => await UserRecoveryService.DeriveRecoveryKeyAsync(masterKey, args.Username, args.Password)) .MatchAsync( () => "An error occurred", x => x.ToBase64String()); From 3ede2f63b3facb9339609bdee14208f073b42330 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Jan 2025 11:26:25 -0600 Subject: [PATCH 06/23] Reorganize UserKeysService and UserRecoveryService with EventfulUserKeysService --- .../Services/IEventfulUserKeysService.cs | 35 +++++ .../Interfaces/Services/IUserKeysService.cs | 26 +++- .../Services/IUserRecoveryService.cs | 20 +-- .../MemoryUserSessionRepository.cs | 53 +++++++ .../Services/EventfulUserKeysService.cs | 108 ++++++++++++++ .../Services/UserKeysService.cs | 136 ++++++------------ .../Services/UserRecoveryService.cs | 18 +-- .../SubmitRecovery_Tests.cs | 63 +++----- Crypter.Web/Shared/MainLayout.razor.cs | 10 +- .../UserSettings/UserSettingsKeys.razor.cs | 6 +- 10 files changed, 290 insertions(+), 185 deletions(-) create mode 100644 Crypter.Common.Client/Interfaces/Services/IEventfulUserKeysService.cs create mode 100644 Crypter.Common.Client/Repositories/MemoryUserSessionRepository.cs create mode 100644 Crypter.Common.Client/Services/EventfulUserKeysService.cs diff --git a/Crypter.Common.Client/Interfaces/Services/IEventfulUserKeysService.cs b/Crypter.Common.Client/Interfaces/Services/IEventfulUserKeysService.cs new file mode 100644 index 000000000..3f3193284 --- /dev/null +++ b/Crypter.Common.Client/Interfaces/Services/IEventfulUserKeysService.cs @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 Crypter File Transfer + * + * This file is part of the Crypter file transfer project. + * + * Crypter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Crypter source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the aforementioned license + * by purchasing a commercial license. Buying such a license is mandatory + * as soon as you develop commercial activities involving the Crypter source + * code without disclosing the source code of your own applications. + * + * Contact the current copyright holder to discuss commercial license options. + */ + +using System; +using Crypter.Common.Client.Events; + +namespace Crypter.Common.Client.Interfaces.Services; + +public interface IEventfulUserKeysService +{ + event EventHandler EmitRecoveryKeyEventHandler; +} diff --git a/Crypter.Common.Client/Interfaces/Services/IUserKeysService.cs b/Crypter.Common.Client/Interfaces/Services/IUserKeysService.cs index 737920344..442c96071 100644 --- a/Crypter.Common.Client/Interfaces/Services/IUserKeysService.cs +++ b/Crypter.Common.Client/Interfaces/Services/IUserKeysService.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -24,8 +24,10 @@ * Contact the current copyright holder to discuss commercial license options. */ -using System; -using Crypter.Common.Client.Events; +using System.Threading.Tasks; +using Crypter.Common.Client.Models; +using Crypter.Common.Contracts.Features.UserAuthentication; +using Crypter.Common.Primitives; using EasyMonads; namespace Crypter.Common.Client.Interfaces.Services; @@ -34,5 +36,21 @@ public interface IUserKeysService { Maybe MasterKey { get; } Maybe PrivateKey { get; } - event EventHandler EmitRecoveryKeyEventHandler; + + /// + /// Derive a recovery key from the provided parameters. + /// + /// + /// + /// A valid, plaintext password. + /// + Task> DeriveRecoveryKeyAsync(byte[] masterKey, Username username, Password password); + + /// + /// Derive a recovery key from the provided parameters. + /// + /// + /// A hashed password. + /// + Task> DeriveRecoveryKeyAsync(byte[] masterKey, VersionedPassword versionedPassword); } diff --git a/Crypter.Common.Client/Interfaces/Services/IUserRecoveryService.cs b/Crypter.Common.Client/Interfaces/Services/IUserRecoveryService.cs index 241bd670c..f75236cd4 100644 --- a/Crypter.Common.Client/Interfaces/Services/IUserRecoveryService.cs +++ b/Crypter.Common.Client/Interfaces/Services/IUserRecoveryService.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -27,7 +27,6 @@ using System.Threading.Tasks; using Crypter.Common.Client.Models; using Crypter.Common.Contracts.Features.AccountRecovery.SubmitRecovery; -using Crypter.Common.Contracts.Features.UserAuthentication; using Crypter.Common.Primitives; using EasyMonads; @@ -35,23 +34,6 @@ namespace Crypter.Common.Client.Interfaces.Services; public interface IUserRecoveryService { - /// - /// Derive a recovery key from the provided parameters. - /// - /// - /// - /// A valid, plaintext password. - /// - Task> DeriveRecoveryKeyAsync(byte[] masterKey, Username username, Password password); - - /// - /// Derive a recovery key from the provided parameters. - /// - /// - /// A hashed password. - /// - Task> DeriveRecoveryKeyAsync(byte[] masterKey, VersionedPassword versionedPassword); - /// /// Request an account recovery email. /// The email, if received, will contain a temporary link to proceed with account recovery. diff --git a/Crypter.Common.Client/Repositories/MemoryUserSessionRepository.cs b/Crypter.Common.Client/Repositories/MemoryUserSessionRepository.cs new file mode 100644 index 000000000..ef7a28d46 --- /dev/null +++ b/Crypter.Common.Client/Repositories/MemoryUserSessionRepository.cs @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2025 Crypter File Transfer + * + * This file is part of the Crypter file transfer project. + * + * Crypter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Crypter source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the aforementioned license + * by purchasing a commercial license. Buying such a license is mandatory + * as soon as you develop commercial activities involving the Crypter source + * code without disclosing the source code of your own applications. + * + * Contact the current copyright holder to discuss commercial license options. + */ + +using System.Threading.Tasks; +using Crypter.Common.Client.Interfaces.Repositories; +using Crypter.Common.Client.Models; +using EasyMonads; + +namespace Crypter.Common.Client.Repositories; + +public class MemoryUserSessionRepository : IUserSessionRepository +{ + Maybe _userSession; + + public MemoryUserSessionRepository() + { + _userSession = Maybe.None; + } + + public Task> GetUserSessionAsync() + { + return _userSession.AsTask(); + } + + public Task StoreUserSessionAsync(UserSession userSession, bool rememberUser) + { + _userSession = userSession; + return Task.CompletedTask; + } +} diff --git a/Crypter.Common.Client/Services/EventfulUserKeysService.cs b/Crypter.Common.Client/Services/EventfulUserKeysService.cs new file mode 100644 index 000000000..485b68102 --- /dev/null +++ b/Crypter.Common.Client/Services/EventfulUserKeysService.cs @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2025 Crypter File Transfer + * + * This file is part of the Crypter file transfer project. + * + * Crypter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Crypter source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the aforementioned license + * by purchasing a commercial license. Buying such a license is mandatory + * as soon as you develop commercial activities involving the Crypter source + * code without disclosing the source code of your own applications. + * + * Contact the current copyright holder to discuss commercial license options. + */ + +using System; +using Crypter.Common.Client.Events; +using Crypter.Common.Client.Interfaces.HttpClients; +using Crypter.Common.Client.Interfaces.Repositories; +using Crypter.Common.Client.Interfaces.Services; +using Crypter.Common.Client.Models; +using Crypter.Crypto.Common; +using EasyMonads; + +namespace Crypter.Common.Client.Services; + +public sealed class EventfulUserKeysService : UserKeysService, IEventfulUserKeysService, IDisposable +{ + private readonly IUserSessionService _userSessionService; + private EventHandler? _emitRecoveryKeyEventHandler; + + public EventfulUserKeysService(ICrypterApiClient crypterApiClient, ICryptoProvider cryptoProvider, IUserPasswordService userPasswordService, IUserKeysRepository userKeysRepository, IUserSessionService userSessionService) + : base(crypterApiClient, cryptoProvider, userPasswordService, userKeysRepository) + { + _userSessionService = userSessionService; + + _userSessionService.ServiceInitializedEventHandler += InitializeAsync; + _userSessionService.UserLoggedInEventHandler += HandleUserLoginAsync; + _userSessionService.UserLoggedOutEventHandler += Recycle; + } + + private async void InitializeAsync(object? _, UserSessionServiceInitializedEventArgs args) + { + if (args.IsLoggedIn) + { + MasterKey = await UserKeysRepository.GetMasterKeyAsync(); + PrivateKey = await UserKeysRepository.GetPrivateKeyAsync(); + } + } + + private async void HandleUserLoginAsync(object? _, UserLoggedInEventArgs args) + { + await UserPasswordService.DeriveUserCredentialKeyAsync(args.Username, args.Password, UserPasswordService.CurrentPasswordVersion) + .BindAsync(async credentialKey => await GetOrCreateMasterKeyAsync(args.VersionedPassword, credentialKey) + .BindAsync(x => new { CredentialKey = credentialKey, x.MasterKey, x.NewRecoveryKey })) + .BindAsync(async carryData => await GetOrCreateKeyPairAsync(carryData.MasterKey) + .BindAsync(x => new { carryData.CredentialKey, carryData.MasterKey, x.PrivateKey, carryData.NewRecoveryKey })) + .IfSomeAsync(async carryData => + { + await StoreSecretKeysAsync(carryData.MasterKey, carryData.PrivateKey, args.RememberUser); + await carryData.NewRecoveryKey + .IfSome(HandleEmitRecoveryKeyEvent) + .IfNoneAsync(async () => + { + if (args.ShowRecoveryKeyModal) + { + await DeriveRecoveryKeyAsync(carryData.MasterKey, args.VersionedPassword) + .IfSomeAsync(HandleEmitRecoveryKeyEvent); + } + }); + }); + } + + private void HandleEmitRecoveryKeyEvent(RecoveryKey recoveryKey) => + _emitRecoveryKeyEventHandler?.Invoke(this, new EmitRecoveryKeyEventArgs(recoveryKey)); + + public event EventHandler EmitRecoveryKeyEventHandler + { + add => _emitRecoveryKeyEventHandler = + (EventHandler)Delegate.Combine(_emitRecoveryKeyEventHandler, value); + remove => _emitRecoveryKeyEventHandler = + (EventHandler?)Delegate.Remove(_emitRecoveryKeyEventHandler, value); + } + + private void Recycle(object? _, EventArgs __) + { + MasterKey = Maybe.None; + PrivateKey = Maybe.None; + } + + public void Dispose() + { + _userSessionService.ServiceInitializedEventHandler -= InitializeAsync; + _userSessionService.UserLoggedInEventHandler -= HandleUserLoginAsync; + _userSessionService.UserLoggedOutEventHandler -= Recycle; + } +} diff --git a/Crypter.Common.Client/Services/UserKeysService.cs b/Crypter.Common.Client/Services/UserKeysService.cs index 40838bac3..6e383e27b 100644 --- a/Crypter.Common.Client/Services/UserKeysService.cs +++ b/Crypter.Common.Client/Services/UserKeysService.cs @@ -24,9 +24,7 @@ * Contact the current copyright holder to discuss commercial license options. */ -using System; using System.Threading.Tasks; -using Crypter.Common.Client.Events; using Crypter.Common.Client.Interfaces.HttpClients; using Crypter.Common.Client.Interfaces.Repositories; using Crypter.Common.Client.Interfaces.Services; @@ -40,64 +38,37 @@ namespace Crypter.Common.Client.Services; -public class UserKeysService : IUserKeysService, IDisposable +public class UserKeysService : IUserKeysService { - private readonly ICrypterApiClient _crypterApiClient; - private readonly ICryptoProvider _cryptoProvider; - private readonly IUserPasswordService _userPasswordService; - private readonly IUserKeysRepository _userKeysRepository; - private readonly IUserSessionService _userSessionService; - private readonly IUserRecoveryService _userRecoveryService; - - private EventHandler? _emitRecoveryKeyEventHandler; + protected readonly ICrypterApiClient CrypterApiClient; + protected readonly ICryptoProvider CryptoProvider; + protected readonly IUserPasswordService UserPasswordService; + protected readonly IUserKeysRepository UserKeysRepository; - public Maybe MasterKey { get; private set; } = Maybe.None; - public Maybe PrivateKey { get; private set; } = Maybe.None; + public Maybe MasterKey { get; protected set; } = Maybe.None; + public Maybe PrivateKey { get; protected set; } = Maybe.None; - public UserKeysService(ICrypterApiClient crypterApiClient, ICryptoProvider cryptoProvider, IUserPasswordService userPasswordService, IUserKeysRepository userKeysRepository, IUserSessionService userSessionService, IUserRecoveryService userRecoveryService) + public UserKeysService(ICrypterApiClient crypterApiClient, ICryptoProvider cryptoProvider, IUserPasswordService userPasswordService, IUserKeysRepository userKeysRepository) { - _crypterApiClient = crypterApiClient; - _userPasswordService = userPasswordService; - _cryptoProvider = cryptoProvider; - _userKeysRepository = userKeysRepository; - _userSessionService = userSessionService; - _userRecoveryService = userRecoveryService; - - _userSessionService.ServiceInitializedEventHandler += InitializeAsync; - _userSessionService.UserLoggedInEventHandler += HandleUserLoginAsync; - _userSessionService.UserLoggedOutEventHandler += Recycle; + CrypterApiClient = crypterApiClient; + UserPasswordService = userPasswordService; + CryptoProvider = cryptoProvider; + UserKeysRepository = userKeysRepository; } - private async void InitializeAsync(object? _, UserSessionServiceInitializedEventArgs args) + public Task> DeriveRecoveryKeyAsync(byte[] masterKey, Username username, Password password) { - if (args.IsLoggedIn) - { - MasterKey = await _userKeysRepository.GetMasterKeyAsync(); - PrivateKey = await _userKeysRepository.GetPrivateKeyAsync(); - } + return UserPasswordService + .DeriveUserAuthenticationPasswordAsync(username, password, UserPasswordService.CurrentPasswordVersion) + .BindAsync(versionedPassword => DeriveRecoveryKeyAsync(masterKey, versionedPassword)); } - private async void HandleUserLoginAsync(object? _, UserLoggedInEventArgs args) + public Task> DeriveRecoveryKeyAsync(byte[] masterKey, VersionedPassword versionedPassword) { - await _userPasswordService.DeriveUserCredentialKeyAsync(args.Username, args.Password, _userPasswordService.CurrentPasswordVersion) - .BindAsync(async credentialKey => await GetOrCreateMasterKeyAsync(args.VersionedPassword, credentialKey) - .BindAsync(x => new { CredentialKey = credentialKey, x.MasterKey, x.NewRecoveryKey })) - .BindAsync(async carryData => await GetOrCreateKeyPairAsync(carryData.MasterKey) - .BindAsync(x => new { carryData.CredentialKey, carryData.MasterKey, x.PrivateKey, carryData.NewRecoveryKey })) - .IfSomeAsync(async carryData => - { - await StoreSecretKeysAsync(carryData.MasterKey, carryData.PrivateKey, args.RememberUser); - await carryData.NewRecoveryKey - .IfSome(HandleEmitRecoveryKeyEvent) - .IfNoneAsync(async () => - { - if (args.ShowRecoveryKeyModal) - { - await _userRecoveryService.DeriveRecoveryKeyAsync(carryData.MasterKey, args.VersionedPassword) - .IfSomeAsync(HandleEmitRecoveryKeyEvent); - } - }); - }); + GetMasterKeyRecoveryProofRequest request = new GetMasterKeyRecoveryProofRequest(versionedPassword.Password); + return CrypterApiClient.UserKey.GetMasterKeyRecoveryProofAsync(request) + .ToMaybeTask() + .MapAsync(x => new RecoveryKey(masterKey, x.Proof)); } /// @@ -107,12 +78,12 @@ await _userRecoveryService.DeriveRecoveryKeyAsync(carryData.MasterKey, args.Vers /// /// /// - private async Task> GetOrCreateMasterKeyAsync(VersionedPassword versionedPassword, byte[] credentialKey) + protected async Task> GetOrCreateMasterKeyAsync(VersionedPassword versionedPassword, byte[] credentialKey) { - return await _crypterApiClient.UserKey.GetMasterKeyAsync() + return await CrypterApiClient.UserKey.GetMasterKeyAsync() .BindAsync(x => { - byte[] decryptedMasterKey = _cryptoProvider.Encryption.Decrypt(credentialKey, x.Nonce, x.EncryptedKey); + byte[] decryptedMasterKey = CryptoProvider.Encryption.Decrypt(credentialKey, x.Nonce, x.EncryptedKey); return new GetOrCreateMasterKeyResult(decryptedMasterKey, Maybe.None); }) .MatchAsync( @@ -134,12 +105,12 @@ private async Task> GetOrCreateMasterKeyAsync( /// /// /// - private async Task> GetOrCreateKeyPairAsync(byte[] masterKey) + protected async Task> GetOrCreateKeyPairAsync(byte[] masterKey) { - return await _crypterApiClient.UserKey.GetPrivateKeyAsync() + return await CrypterApiClient.UserKey.GetPrivateKeyAsync() .BindAsync(x => { - byte[] decryptedPrivateKey = _cryptoProvider.Encryption.Decrypt(masterKey, x.Nonce, x.EncryptedKey); + byte[] decryptedPrivateKey = CryptoProvider.Encryption.Decrypt(masterKey, x.Nonce, x.EncryptedKey); return new GetOrCreateKeyPairResult(decryptedPrivateKey); }) .MatchAsync( @@ -157,63 +128,38 @@ private async Task> GetOrCreateKeyPairAsync(byte private Task> UploadNewMasterKeyAsync(VersionedPassword versionedPassword, byte[] credentialKey) { - byte[] newMasterKey = _cryptoProvider.Random.GenerateRandomBytes((int)_cryptoProvider.Encryption.KeySize); - byte[] nonce = _cryptoProvider.Random.GenerateRandomBytes((int)_cryptoProvider.Encryption.NonceSize); - byte[] encryptedMasterKey = _cryptoProvider.Encryption.Encrypt(credentialKey, nonce, newMasterKey); - byte[] recoveryProof = _cryptoProvider.Random.GenerateRandomBytes(32); + byte[] newMasterKey = CryptoProvider.Random.GenerateRandomBytes((int)CryptoProvider.Encryption.KeySize); + byte[] nonce = CryptoProvider.Random.GenerateRandomBytes((int)CryptoProvider.Encryption.NonceSize); + byte[] encryptedMasterKey = CryptoProvider.Encryption.Encrypt(credentialKey, nonce, newMasterKey); + byte[] recoveryProof = CryptoProvider.Random.GenerateRandomBytes(32); InsertMasterKeyRequest request = new InsertMasterKeyRequest(versionedPassword.Password, encryptedMasterKey, nonce, recoveryProof); - return _crypterApiClient.UserKey.InsertMasterKeyAsync(request) + return CrypterApiClient.UserKey.InsertMasterKeyAsync(request) .ToMaybeTask() .BindAsync(_ => new GetOrCreateMasterKeyResult(newMasterKey, new RecoveryKey(newMasterKey, request.RecoveryProof))); } private Task> UploadNewUserKeyPairAsync(byte[] masterKey) { - X25519KeyPair keyPair = _cryptoProvider.KeyExchange.GenerateKeyPair(); - byte[] nonce = _cryptoProvider.Random.GenerateRandomBytes((int)_cryptoProvider.Encryption.NonceSize); - byte[] encryptedPrivateKey = _cryptoProvider.Encryption.Encrypt(masterKey, nonce, keyPair.PrivateKey); + X25519KeyPair keyPair = CryptoProvider.KeyExchange.GenerateKeyPair(); + byte[] nonce = CryptoProvider.Random.GenerateRandomBytes((int)CryptoProvider.Encryption.NonceSize); + byte[] encryptedPrivateKey = CryptoProvider.Encryption.Encrypt(masterKey, nonce, keyPair.PrivateKey); InsertKeyPairRequest request = new InsertKeyPairRequest(encryptedPrivateKey, keyPair.PublicKey, nonce); - return _crypterApiClient.UserKey.InsertKeyPairAsync(request) + return CrypterApiClient.UserKey.InsertKeyPairAsync(request) .ToMaybeTask() .BindAsync(_ => new GetOrCreateKeyPairResult(keyPair.PrivateKey)); } - private async Task StoreSecretKeysAsync(byte[] masterKey, byte[] privateKey, bool trustDevice) + protected async Task StoreSecretKeysAsync(byte[] masterKey, byte[] privateKey, bool trustDevice) { MasterKey = masterKey; PrivateKey = privateKey; - await _userKeysRepository.StoreMasterKeyAsync(masterKey, trustDevice); - await _userKeysRepository.StorePrivateKeyAsync(privateKey, trustDevice); - } - - private void HandleEmitRecoveryKeyEvent(RecoveryKey recoveryKey) => - _emitRecoveryKeyEventHandler?.Invoke(this, new EmitRecoveryKeyEventArgs(recoveryKey)); - - public event EventHandler EmitRecoveryKeyEventHandler - { - add => _emitRecoveryKeyEventHandler = - (EventHandler)Delegate.Combine(_emitRecoveryKeyEventHandler, value); - remove => _emitRecoveryKeyEventHandler = - (EventHandler?)Delegate.Remove(_emitRecoveryKeyEventHandler, value); - } - - private void Recycle(object? _, EventArgs __) - { - MasterKey = Maybe.None; - PrivateKey = Maybe.None; - } - - public void Dispose() - { - _userSessionService.ServiceInitializedEventHandler -= InitializeAsync; - _userSessionService.UserLoggedInEventHandler -= HandleUserLoginAsync; - _userSessionService.UserLoggedOutEventHandler -= Recycle; - GC.SuppressFinalize(this); + await UserKeysRepository.StoreMasterKeyAsync(masterKey, trustDevice); + await UserKeysRepository.StorePrivateKeyAsync(privateKey, trustDevice); } - private sealed record GetOrCreateMasterKeyResult + protected sealed record GetOrCreateMasterKeyResult { public byte[] MasterKey { get; } public Maybe NewRecoveryKey { get; } @@ -225,7 +171,7 @@ public GetOrCreateMasterKeyResult(byte[] masterKey, Maybe newRecove } } - private sealed record GetOrCreateKeyPairResult + protected sealed record GetOrCreateKeyPairResult { public byte[] PrivateKey { get; } diff --git a/Crypter.Common.Client/Services/UserRecoveryService.cs b/Crypter.Common.Client/Services/UserRecoveryService.cs index beb05072a..ab67826cd 100644 --- a/Crypter.Common.Client/Services/UserRecoveryService.cs +++ b/Crypter.Common.Client/Services/UserRecoveryService.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -29,7 +29,6 @@ using Crypter.Common.Client.Interfaces.Services; using Crypter.Common.Client.Models; using Crypter.Common.Contracts.Features.AccountRecovery.SubmitRecovery; -using Crypter.Common.Contracts.Features.Keys; using Crypter.Common.Contracts.Features.UserAuthentication; using Crypter.Common.Primitives; using Crypter.Crypto.Common; @@ -106,21 +105,6 @@ public async Task>> Submit .Select(y => y.NewRecoveryKey)); }); } - - public Task> DeriveRecoveryKeyAsync(byte[] masterKey, Username username, Password password) - { - return _userPasswordService - .DeriveUserAuthenticationPasswordAsync(username, password, _userPasswordService.CurrentPasswordVersion) - .BindAsync(versionedPassword => DeriveRecoveryKeyAsync(masterKey, versionedPassword)); - } - - public Task> DeriveRecoveryKeyAsync(byte[] masterKey, VersionedPassword versionedPassword) - { - GetMasterKeyRecoveryProofRequest request = new GetMasterKeyRecoveryProofRequest(versionedPassword.Password); - return _crypterApiClient.UserKey.GetMasterKeyRecoveryProofAsync(request) - .ToMaybeTask() - .MapAsync(x => new RecoveryKey(masterKey, x.Proof)); - } private struct PasswordDerivatives { diff --git a/Crypter.Test/Integration_Tests/UserRecovery_Tests/SubmitRecovery_Tests.cs b/Crypter.Test/Integration_Tests/UserRecovery_Tests/SubmitRecovery_Tests.cs index b9531952c..90954458c 100644 --- a/Crypter.Test/Integration_Tests/UserRecovery_Tests/SubmitRecovery_Tests.cs +++ b/Crypter.Test/Integration_Tests/UserRecovery_Tests/SubmitRecovery_Tests.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -24,13 +24,13 @@ * Contact the current copyright holder to discuss commercial license options. */ -using System; using System.Linq; using System.Text; using System.Threading.Tasks; using Crypter.Common.Client.Interfaces.HttpClients; using Crypter.Common.Client.Interfaces.Repositories; using Crypter.Common.Client.Models; +using Crypter.Common.Client.Repositories; using Crypter.Common.Client.Services; using Crypter.Common.Contracts.Features.AccountRecovery.RequestRecovery; using Crypter.Common.Contracts.Features.AccountRecovery.SubmitRecovery; @@ -50,7 +50,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; -using UserRecoveryService = Crypter.Common.Client.Services.UserRecoveryService; namespace Crypter.Test.Integration_Tests.UserRecovery_Tests; @@ -116,8 +115,9 @@ await loginResult.DoRightAsync(async loginResponse => Either _ = await _client!.UserKey.InsertMasterKeyAsync(insertMasterKeyRequest); UserPasswordService userPasswordService = new UserPasswordService(_cryptoProvider!); - UserRecoveryService userRecoveryService = new UserRecoveryService(_client, new DefaultCryptoProvider(), userPasswordService); - recoveryKey = await userRecoveryService.DeriveRecoveryKeyAsync(masterKey, registrationRequest.VersionedPassword); + IUserKeysRepository userKeysRepository = new MemoryKeysRepository(); + UserKeysService userKeysService = new UserKeysService(_client, new DefaultCryptoProvider(), userPasswordService, userKeysRepository); + recoveryKey = await userKeysService.DeriveRecoveryKeyAsync(masterKey, registrationRequest.VersionedPassword); recoveryKey.IfNone(Assert.Fail); } @@ -145,22 +145,16 @@ await loginResult.DoRightAsync(async loginResponse => .FirstAsync(); string encodedRecoveryCode = UrlSafeEncoder.EncodeGuidUrlSafe(recoveryData.Code); - byte[] signedRecoveryData = Core.Features.AccountRecovery.Common.GenerateRecoverySignature(_cryptoProvider!, _knownKeyPair!.PrivateKey, - recoveryData.Code, Username.From(TestData.DefaultUsername)); + byte[] signedRecoveryData = Core.Features.AccountRecovery.Common.GenerateRecoverySignature(_cryptoProvider!, _knownKeyPair!.PrivateKey, recoveryData.Code, Username.From(TestData.DefaultUsername)); string encodedRecoverySignature = UrlSafeEncoder.EncodeBytesUrlSafe(signedRecoveryData); - VersionedPassword versionedPassword = - new VersionedPassword(Encoding.UTF8.GetBytes(TestData.DefaultPassword), 1); + VersionedPassword versionedPassword = new VersionedPassword(Encoding.UTF8.GetBytes(TestData.DefaultPassword), 1); AccountRecoverySubmission recoverySubmission = recoveryKey - .Bind(x => new ReplacementMasterKeyInformation( - x.Proof, [0x01], [0x02], - [0x03])) + .Bind(x => new ReplacementMasterKeyInformation(x.Proof, [0x01], [0x02], [0x03])) .Match( - none: new AccountRecoverySubmission(TestData.DefaultUsername, encodedRecoveryCode, - encodedRecoverySignature, versionedPassword), - some: x => new AccountRecoverySubmission(TestData.DefaultUsername, encodedRecoveryCode, - encodedRecoverySignature, versionedPassword, x)); + none: new AccountRecoverySubmission(TestData.DefaultUsername, encodedRecoveryCode, encodedRecoverySignature, versionedPassword), + some: x => new AccountRecoverySubmission(TestData.DefaultUsername, encodedRecoveryCode, encodedRecoverySignature, versionedPassword, x)); Either result = await _client!.UserRecovery.SubmitRecoveryAsync(recoverySubmission); @@ -174,10 +168,8 @@ await loginResult.DoRightAsync(async loginResponse => [Test] public async Task Submit_Recovery_Fails_When_Recovery_Code_Provided_With_Empty_New_Master_Key_Information() { - RegistrationRequest registrationRequest = TestData.GetRegistrationRequest(TestData.DefaultUsername, - TestData.DefaultPassword, TestData.DefaultEmailAdress); - Either registrationResult = - await _client!.UserAuthentication.RegisterAsync(registrationRequest); + RegistrationRequest registrationRequest = TestData.GetRegistrationRequest(TestData.DefaultUsername, TestData.DefaultPassword, TestData.DefaultEmailAdress); + Either registrationResult = await _client!.UserAuthentication.RegisterAsync(registrationRequest); // Allow the background service to "send" the verification email and save the email verification data await Task.Delay(5000); @@ -194,20 +186,16 @@ public async Task Submit_Recovery_Fails_When_Recovery_Code_Provided_With_Empty_N verificationData.Code.ToByteArray()); string encodedVerificationSignature = UrlSafeEncoder.EncodeBytesUrlSafe(signedVerificationCode); - VerifyEmailAddressRequest verificationRequest = - new VerifyEmailAddressRequest(encodedVerificationCode, encodedVerificationSignature); - Either verificationResult = - await _client!.UserSetting.VerifyUserEmailAddressAsync(verificationRequest); + VerifyEmailAddressRequest verificationRequest = new VerifyEmailAddressRequest(encodedVerificationCode, encodedVerificationSignature); + Either verificationResult = await _client!.UserSetting.VerifyUserEmailAddressAsync(verificationRequest); EmailAddress emailAddress = EmailAddress.From(TestData.DefaultEmailAdress); - Either sendRecoveryEmailResult = - await _client!.UserRecovery.SendRecoveryEmailAsync(emailAddress); + Either sendRecoveryEmailResult = await _client!.UserRecovery.SendRecoveryEmailAsync(emailAddress); // Allow the background service to "send" the recovery email and save the recovery data await Task.Delay(5000); - LoginRequest loginRequest = - TestData.GetLoginRequest(TestData.DefaultUsername, TestData.DefaultPassword); + LoginRequest loginRequest = TestData.GetLoginRequest(TestData.DefaultUsername, TestData.DefaultPassword); Either loginResult = await _client!.UserAuthentication.LoginAsync(loginRequest); await loginResult.DoRightAsync(async loginResponse => @@ -216,16 +204,13 @@ await loginResult.DoRightAsync(async loginResponse => await _clientTokenRepository!.StoreRefreshTokenAsync(loginResponse.RefreshToken, TokenType.Session); }); - (byte[] masterKey, InsertMasterKeyRequest insertMasterKeyRequest) = - TestData.GetInsertMasterKeyRequest(TestData.DefaultPassword); - Either _ = - await _client!.UserKey.InsertMasterKeyAsync(insertMasterKeyRequest); + (byte[] masterKey, InsertMasterKeyRequest insertMasterKeyRequest) = TestData.GetInsertMasterKeyRequest(TestData.DefaultPassword); + Either _ = await _client!.UserKey.InsertMasterKeyAsync(insertMasterKeyRequest); UserPasswordService userPasswordService = new UserPasswordService(_cryptoProvider!); - UserRecoveryService userRecoveryService = - new UserRecoveryService(_client, new DefaultCryptoProvider(), userPasswordService); - Maybe recoveryKeyResponse = - await userRecoveryService.DeriveRecoveryKeyAsync(masterKey, registrationRequest.VersionedPassword); + IUserKeysRepository userKeysRepository = new MemoryKeysRepository(); + UserKeysService userKeysService = new UserKeysService(_client, new DefaultCryptoProvider(), userPasswordService, userKeysRepository); + Maybe recoveryKeyResponse = await userKeysService.DeriveRecoveryKeyAsync(masterKey, registrationRequest.VersionedPassword); await recoveryKeyResponse .IfNone(Assert.Fail) @@ -240,11 +225,9 @@ await recoveryKeyResponse recoveryData.Code, Username.From(TestData.DefaultUsername)); string encodedRecoverySignature = UrlSafeEncoder.EncodeBytesUrlSafe(signedRecoveryData); - VersionedPassword versionedPassword = - new VersionedPassword(Encoding.UTF8.GetBytes(TestData.DefaultPassword), 1); + VersionedPassword versionedPassword = new VersionedPassword(Encoding.UTF8.GetBytes(TestData.DefaultPassword), 1); - ReplacementMasterKeyInformation replacementMasterKeyInformation = - new ReplacementMasterKeyInformation(recoveryKey.Proof, [], [], []); + ReplacementMasterKeyInformation replacementMasterKeyInformation = new ReplacementMasterKeyInformation(recoveryKey.Proof, [], [], []); AccountRecoverySubmission submission = new AccountRecoverySubmission(TestData.DefaultUsername, encodedRecoveryCode, encodedRecoverySignature, versionedPassword, replacementMasterKeyInformation); diff --git a/Crypter.Web/Shared/MainLayout.razor.cs b/Crypter.Web/Shared/MainLayout.razor.cs index 606e7766c..bbf53a34d 100644 --- a/Crypter.Web/Shared/MainLayout.razor.cs +++ b/Crypter.Web/Shared/MainLayout.razor.cs @@ -43,10 +43,8 @@ namespace Crypter.Web.Shared; public class MainLayoutBase : LayoutComponentBase, IDisposable { [Inject] private IBlazorSodiumService BlazorSodiumService { get; init; } = null!; - - [Inject] private IUserSessionService UserSessionService { get; init; } = null!; - - [Inject] private IUserKeysService UserKeysService { get; init; } = null!; + + [Inject] private IEventfulUserKeysService EventfulUserKeysService { get; init; } = null!; [Inject] private IUserPasswordService UserPasswordService { get; init; } = null!; @@ -63,7 +61,7 @@ public class MainLayoutBase : LayoutComponentBase, IDisposable protected override async Task OnInitializedAsync() { - UserKeysService.EmitRecoveryKeyEventHandler += HandleRecoveryKeyCreatedEvent; + EventfulUserKeysService.EmitRecoveryKeyEventHandler += HandleRecoveryKeyCreatedEvent; UserPasswordService.PasswordHashBeginEventHandler += ShowPasswordHashingModal; UserPasswordService.PasswordHashEndEventHandler += ClosePasswordHashingModal; await BrowserRepository.InitializeAsync(); @@ -103,7 +101,7 @@ private async void ClosePasswordHashingModal(object? _, PasswordHashEndEventArgs public void Dispose() { - UserKeysService.EmitRecoveryKeyEventHandler -= HandleRecoveryKeyCreatedEvent; + EventfulUserKeysService.EmitRecoveryKeyEventHandler -= HandleRecoveryKeyCreatedEvent; UserPasswordService.PasswordHashBeginEventHandler -= ShowPasswordHashingModal; UserPasswordService.PasswordHashEndEventHandler -= ClosePasswordHashingModal; GC.SuppressFinalize(this); diff --git a/Crypter.Web/Shared/UserSettings/UserSettingsKeys.razor.cs b/Crypter.Web/Shared/UserSettings/UserSettingsKeys.razor.cs index c0d8facec..edcd3eebe 100644 --- a/Crypter.Web/Shared/UserSettings/UserSettingsKeys.razor.cs +++ b/Crypter.Web/Shared/UserSettings/UserSettingsKeys.razor.cs @@ -40,9 +40,7 @@ public partial class UserSettingsKeys : IDisposable [Inject] private IUserSessionService UserSessionService { get; init; } = null!; [Inject] private IUserKeysService UserKeysService { get; init; } = null!; - - [Inject] private IUserRecoveryService UserRecoveryService { get; init; } = null!; - + [Inject] private IJSRuntime JsRuntime { get; init; } = null!; private PasswordChallengeModal _passwordModal = null!; @@ -62,7 +60,7 @@ protected override void OnInitialized() private async void OnPasswordTestSuccess(object? sender, UserPasswordTestSuccessEventArgs args) { _recoveryKey = await UserKeysService.MasterKey - .BindAsync(async masterKey => await UserRecoveryService.DeriveRecoveryKeyAsync(masterKey, args.Username, args.Password)) + .BindAsync(async masterKey => await UserKeysService.DeriveRecoveryKeyAsync(masterKey, args.Username, args.Password)) .MatchAsync( () => "An error occurred", x => x.ToBase64String()); From 36b41fddf6c66a7a6c0789bc6e641b013fc28b8b Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Jan 2025 11:39:00 -0600 Subject: [PATCH 07/23] EventfulUserKeysService emits keys progress to MainLayout --- .../Services/IEventfulUserKeysService.cs | 2 ++ .../Services/EventfulUserKeysService.cs | 26 +++++++++++++++++++ Crypter.Web/Client/Program.cs | 1 + Crypter.Web/Shared/MainLayout.razor.cs | 12 +++++++++ .../Shared/Modal/SpinnerModal.razor.cs | 3 +-- 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Crypter.Common.Client/Interfaces/Services/IEventfulUserKeysService.cs b/Crypter.Common.Client/Interfaces/Services/IEventfulUserKeysService.cs index 3f3193284..fb05f3fef 100644 --- a/Crypter.Common.Client/Interfaces/Services/IEventfulUserKeysService.cs +++ b/Crypter.Common.Client/Interfaces/Services/IEventfulUserKeysService.cs @@ -32,4 +32,6 @@ namespace Crypter.Common.Client.Interfaces.Services; public interface IEventfulUserKeysService { event EventHandler EmitRecoveryKeyEventHandler; + event EventHandler PrepareUserKeysBeginEventHandler; + event EventHandler PrepareUserKeysEndEventHandler; } diff --git a/Crypter.Common.Client/Services/EventfulUserKeysService.cs b/Crypter.Common.Client/Services/EventfulUserKeysService.cs index 485b68102..3c252015e 100644 --- a/Crypter.Common.Client/Services/EventfulUserKeysService.cs +++ b/Crypter.Common.Client/Services/EventfulUserKeysService.cs @@ -39,6 +39,8 @@ public sealed class EventfulUserKeysService : UserKeysService, IEventfulUserKeys { private readonly IUserSessionService _userSessionService; private EventHandler? _emitRecoveryKeyEventHandler; + private event EventHandler? _prepareUserKeysBeginEventHandler; + private event EventHandler? _prepareUserKeysEndEventHandler; public EventfulUserKeysService(ICrypterApiClient crypterApiClient, ICryptoProvider cryptoProvider, IUserPasswordService userPasswordService, IUserKeysRepository userKeysRepository, IUserSessionService userSessionService) : base(crypterApiClient, cryptoProvider, userPasswordService, userKeysRepository) @@ -62,6 +64,7 @@ private async void InitializeAsync(object? _, UserSessionServiceInitializedEvent private async void HandleUserLoginAsync(object? _, UserLoggedInEventArgs args) { await UserPasswordService.DeriveUserCredentialKeyAsync(args.Username, args.Password, UserPasswordService.CurrentPasswordVersion) + .IfSomeAsync(_ => HandlePrepareUserKeysBeginEvent()) .BindAsync(async credentialKey => await GetOrCreateMasterKeyAsync(args.VersionedPassword, credentialKey) .BindAsync(x => new { CredentialKey = credentialKey, x.MasterKey, x.NewRecoveryKey })) .BindAsync(async carryData => await GetOrCreateKeyPairAsync(carryData.MasterKey) @@ -69,6 +72,7 @@ await UserPasswordService.DeriveUserCredentialKeyAsync(args.Username, args.Passw .IfSomeAsync(async carryData => { await StoreSecretKeysAsync(carryData.MasterKey, carryData.PrivateKey, args.RememberUser); + HandlePrepareUserKeysEndEvent(); await carryData.NewRecoveryKey .IfSome(HandleEmitRecoveryKeyEvent) .IfNoneAsync(async () => @@ -85,6 +89,12 @@ await DeriveRecoveryKeyAsync(carryData.MasterKey, args.VersionedPassword) private void HandleEmitRecoveryKeyEvent(RecoveryKey recoveryKey) => _emitRecoveryKeyEventHandler?.Invoke(this, new EmitRecoveryKeyEventArgs(recoveryKey)); + private void HandlePrepareUserKeysBeginEvent() => + _prepareUserKeysBeginEventHandler?.Invoke(this, EventArgs.Empty); + + private void HandlePrepareUserKeysEndEvent() => + _prepareUserKeysEndEventHandler?.Invoke(this, EventArgs.Empty); + public event EventHandler EmitRecoveryKeyEventHandler { add => _emitRecoveryKeyEventHandler = @@ -93,6 +103,22 @@ public event EventHandler EmitRecoveryKeyEventHandler (EventHandler?)Delegate.Remove(_emitRecoveryKeyEventHandler, value); } + public event EventHandler PrepareUserKeysBeginEventHandler + { + add => _prepareUserKeysBeginEventHandler = + (EventHandler)Delegate.Combine(_prepareUserKeysBeginEventHandler, value); + remove => _prepareUserKeysBeginEventHandler = + (EventHandler?)Delegate.Remove(_prepareUserKeysBeginEventHandler, value); + } + + public event EventHandler PrepareUserKeysEndEventHandler + { + add => _prepareUserKeysEndEventHandler = + (EventHandler)Delegate.Combine(_prepareUserKeysEndEventHandler, value); + remove => _prepareUserKeysEndEventHandler = + (EventHandler?)Delegate.Remove(_prepareUserKeysEndEventHandler, value); + } + private void Recycle(object? _, EventArgs __) { MasterKey = Maybe.None; diff --git a/Crypter.Web/Client/Program.cs b/Crypter.Web/Client/Program.cs index 90f2a634e..6dafb8ec3 100644 --- a/Crypter.Web/Client/Program.cs +++ b/Crypter.Web/Client/Program.cs @@ -97,6 +97,7 @@ .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Crypter.Web/Shared/MainLayout.razor.cs b/Crypter.Web/Shared/MainLayout.razor.cs index bbf53a34d..d0786a173 100644 --- a/Crypter.Web/Shared/MainLayout.razor.cs +++ b/Crypter.Web/Shared/MainLayout.razor.cs @@ -71,6 +71,18 @@ protected override async Task OnInitializedAsync() ServicesInitialized = true; } + private void ShowUserKeysProgressModal(object? _, EventArgs __) + { + SpinnerModal.Open("Preparing Secret Keys", + "Please wait while your secret keys are being prepared.", + Maybe.None); + } + + private async void CloseUserKeysProgressModal(object? _, EventArgs __) + { + await SpinnerModal.CloseAsync(); + } + private void HandleRecoveryKeyCreatedEvent(object? _, EmitRecoveryKeyEventArgs args) { RecoveryKeyModal.Open(args.RecoveryKey); diff --git a/Crypter.Web/Shared/Modal/SpinnerModal.razor.cs b/Crypter.Web/Shared/Modal/SpinnerModal.razor.cs index 8587385c3..ae172dae1 100644 --- a/Crypter.Web/Shared/Modal/SpinnerModal.razor.cs +++ b/Crypter.Web/Shared/Modal/SpinnerModal.razor.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -24,7 +24,6 @@ * Contact the current copyright holder to discuss commercial license options. */ -using System; using System.Threading.Tasks; using Crypter.Web.Shared.Modal.Template; using EasyMonads; From 1add12c2c6f18d139493e7e319e842fa361dec0b Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Jan 2025 11:47:14 -0600 Subject: [PATCH 08/23] Revert "EventfulUserKeysService emits keys progress to MainLayout" This reverts commit 36b41fddf6c66a7a6c0789bc6e641b013fc28b8b. --- .../Services/IEventfulUserKeysService.cs | 2 -- .../Services/EventfulUserKeysService.cs | 26 ------------------- Crypter.Web/Client/Program.cs | 1 - Crypter.Web/Shared/MainLayout.razor.cs | 12 --------- .../Shared/Modal/SpinnerModal.razor.cs | 3 ++- 5 files changed, 2 insertions(+), 42 deletions(-) diff --git a/Crypter.Common.Client/Interfaces/Services/IEventfulUserKeysService.cs b/Crypter.Common.Client/Interfaces/Services/IEventfulUserKeysService.cs index fb05f3fef..3f3193284 100644 --- a/Crypter.Common.Client/Interfaces/Services/IEventfulUserKeysService.cs +++ b/Crypter.Common.Client/Interfaces/Services/IEventfulUserKeysService.cs @@ -32,6 +32,4 @@ namespace Crypter.Common.Client.Interfaces.Services; public interface IEventfulUserKeysService { event EventHandler EmitRecoveryKeyEventHandler; - event EventHandler PrepareUserKeysBeginEventHandler; - event EventHandler PrepareUserKeysEndEventHandler; } diff --git a/Crypter.Common.Client/Services/EventfulUserKeysService.cs b/Crypter.Common.Client/Services/EventfulUserKeysService.cs index 3c252015e..485b68102 100644 --- a/Crypter.Common.Client/Services/EventfulUserKeysService.cs +++ b/Crypter.Common.Client/Services/EventfulUserKeysService.cs @@ -39,8 +39,6 @@ public sealed class EventfulUserKeysService : UserKeysService, IEventfulUserKeys { private readonly IUserSessionService _userSessionService; private EventHandler? _emitRecoveryKeyEventHandler; - private event EventHandler? _prepareUserKeysBeginEventHandler; - private event EventHandler? _prepareUserKeysEndEventHandler; public EventfulUserKeysService(ICrypterApiClient crypterApiClient, ICryptoProvider cryptoProvider, IUserPasswordService userPasswordService, IUserKeysRepository userKeysRepository, IUserSessionService userSessionService) : base(crypterApiClient, cryptoProvider, userPasswordService, userKeysRepository) @@ -64,7 +62,6 @@ private async void InitializeAsync(object? _, UserSessionServiceInitializedEvent private async void HandleUserLoginAsync(object? _, UserLoggedInEventArgs args) { await UserPasswordService.DeriveUserCredentialKeyAsync(args.Username, args.Password, UserPasswordService.CurrentPasswordVersion) - .IfSomeAsync(_ => HandlePrepareUserKeysBeginEvent()) .BindAsync(async credentialKey => await GetOrCreateMasterKeyAsync(args.VersionedPassword, credentialKey) .BindAsync(x => new { CredentialKey = credentialKey, x.MasterKey, x.NewRecoveryKey })) .BindAsync(async carryData => await GetOrCreateKeyPairAsync(carryData.MasterKey) @@ -72,7 +69,6 @@ await UserPasswordService.DeriveUserCredentialKeyAsync(args.Username, args.Passw .IfSomeAsync(async carryData => { await StoreSecretKeysAsync(carryData.MasterKey, carryData.PrivateKey, args.RememberUser); - HandlePrepareUserKeysEndEvent(); await carryData.NewRecoveryKey .IfSome(HandleEmitRecoveryKeyEvent) .IfNoneAsync(async () => @@ -89,12 +85,6 @@ await DeriveRecoveryKeyAsync(carryData.MasterKey, args.VersionedPassword) private void HandleEmitRecoveryKeyEvent(RecoveryKey recoveryKey) => _emitRecoveryKeyEventHandler?.Invoke(this, new EmitRecoveryKeyEventArgs(recoveryKey)); - private void HandlePrepareUserKeysBeginEvent() => - _prepareUserKeysBeginEventHandler?.Invoke(this, EventArgs.Empty); - - private void HandlePrepareUserKeysEndEvent() => - _prepareUserKeysEndEventHandler?.Invoke(this, EventArgs.Empty); - public event EventHandler EmitRecoveryKeyEventHandler { add => _emitRecoveryKeyEventHandler = @@ -103,22 +93,6 @@ public event EventHandler EmitRecoveryKeyEventHandler (EventHandler?)Delegate.Remove(_emitRecoveryKeyEventHandler, value); } - public event EventHandler PrepareUserKeysBeginEventHandler - { - add => _prepareUserKeysBeginEventHandler = - (EventHandler)Delegate.Combine(_prepareUserKeysBeginEventHandler, value); - remove => _prepareUserKeysBeginEventHandler = - (EventHandler?)Delegate.Remove(_prepareUserKeysBeginEventHandler, value); - } - - public event EventHandler PrepareUserKeysEndEventHandler - { - add => _prepareUserKeysEndEventHandler = - (EventHandler)Delegate.Combine(_prepareUserKeysEndEventHandler, value); - remove => _prepareUserKeysEndEventHandler = - (EventHandler?)Delegate.Remove(_prepareUserKeysEndEventHandler, value); - } - private void Recycle(object? _, EventArgs __) { MasterKey = Maybe.None; diff --git a/Crypter.Web/Client/Program.cs b/Crypter.Web/Client/Program.cs index 6dafb8ec3..90f2a634e 100644 --- a/Crypter.Web/Client/Program.cs +++ b/Crypter.Web/Client/Program.cs @@ -97,7 +97,6 @@ .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Crypter.Web/Shared/MainLayout.razor.cs b/Crypter.Web/Shared/MainLayout.razor.cs index d0786a173..bbf53a34d 100644 --- a/Crypter.Web/Shared/MainLayout.razor.cs +++ b/Crypter.Web/Shared/MainLayout.razor.cs @@ -71,18 +71,6 @@ protected override async Task OnInitializedAsync() ServicesInitialized = true; } - private void ShowUserKeysProgressModal(object? _, EventArgs __) - { - SpinnerModal.Open("Preparing Secret Keys", - "Please wait while your secret keys are being prepared.", - Maybe.None); - } - - private async void CloseUserKeysProgressModal(object? _, EventArgs __) - { - await SpinnerModal.CloseAsync(); - } - private void HandleRecoveryKeyCreatedEvent(object? _, EmitRecoveryKeyEventArgs args) { RecoveryKeyModal.Open(args.RecoveryKey); diff --git a/Crypter.Web/Shared/Modal/SpinnerModal.razor.cs b/Crypter.Web/Shared/Modal/SpinnerModal.razor.cs index ae172dae1..8587385c3 100644 --- a/Crypter.Web/Shared/Modal/SpinnerModal.razor.cs +++ b/Crypter.Web/Shared/Modal/SpinnerModal.razor.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2025 Crypter File Transfer + * Copyright (C) 2024 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -24,6 +24,7 @@ * Contact the current copyright holder to discuss commercial license options. */ +using System; using System.Threading.Tasks; using Crypter.Web.Shared.Modal.Template; using EasyMonads; From 0005e2fc823336d112d42d261be4a7666c489d0b Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Jan 2025 11:47:33 -0600 Subject: [PATCH 09/23] Reapply "EventfulUserKeysService emits keys progress to MainLayout" This reverts commit 1add12c2c6f18d139493e7e319e842fa361dec0b. --- .../Services/IEventfulUserKeysService.cs | 2 ++ .../Services/EventfulUserKeysService.cs | 26 +++++++++++++++++++ Crypter.Web/Client/Program.cs | 1 + Crypter.Web/Shared/MainLayout.razor.cs | 12 +++++++++ .../Shared/Modal/SpinnerModal.razor.cs | 3 +-- 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Crypter.Common.Client/Interfaces/Services/IEventfulUserKeysService.cs b/Crypter.Common.Client/Interfaces/Services/IEventfulUserKeysService.cs index 3f3193284..fb05f3fef 100644 --- a/Crypter.Common.Client/Interfaces/Services/IEventfulUserKeysService.cs +++ b/Crypter.Common.Client/Interfaces/Services/IEventfulUserKeysService.cs @@ -32,4 +32,6 @@ namespace Crypter.Common.Client.Interfaces.Services; public interface IEventfulUserKeysService { event EventHandler EmitRecoveryKeyEventHandler; + event EventHandler PrepareUserKeysBeginEventHandler; + event EventHandler PrepareUserKeysEndEventHandler; } diff --git a/Crypter.Common.Client/Services/EventfulUserKeysService.cs b/Crypter.Common.Client/Services/EventfulUserKeysService.cs index 485b68102..3c252015e 100644 --- a/Crypter.Common.Client/Services/EventfulUserKeysService.cs +++ b/Crypter.Common.Client/Services/EventfulUserKeysService.cs @@ -39,6 +39,8 @@ public sealed class EventfulUserKeysService : UserKeysService, IEventfulUserKeys { private readonly IUserSessionService _userSessionService; private EventHandler? _emitRecoveryKeyEventHandler; + private event EventHandler? _prepareUserKeysBeginEventHandler; + private event EventHandler? _prepareUserKeysEndEventHandler; public EventfulUserKeysService(ICrypterApiClient crypterApiClient, ICryptoProvider cryptoProvider, IUserPasswordService userPasswordService, IUserKeysRepository userKeysRepository, IUserSessionService userSessionService) : base(crypterApiClient, cryptoProvider, userPasswordService, userKeysRepository) @@ -62,6 +64,7 @@ private async void InitializeAsync(object? _, UserSessionServiceInitializedEvent private async void HandleUserLoginAsync(object? _, UserLoggedInEventArgs args) { await UserPasswordService.DeriveUserCredentialKeyAsync(args.Username, args.Password, UserPasswordService.CurrentPasswordVersion) + .IfSomeAsync(_ => HandlePrepareUserKeysBeginEvent()) .BindAsync(async credentialKey => await GetOrCreateMasterKeyAsync(args.VersionedPassword, credentialKey) .BindAsync(x => new { CredentialKey = credentialKey, x.MasterKey, x.NewRecoveryKey })) .BindAsync(async carryData => await GetOrCreateKeyPairAsync(carryData.MasterKey) @@ -69,6 +72,7 @@ await UserPasswordService.DeriveUserCredentialKeyAsync(args.Username, args.Passw .IfSomeAsync(async carryData => { await StoreSecretKeysAsync(carryData.MasterKey, carryData.PrivateKey, args.RememberUser); + HandlePrepareUserKeysEndEvent(); await carryData.NewRecoveryKey .IfSome(HandleEmitRecoveryKeyEvent) .IfNoneAsync(async () => @@ -85,6 +89,12 @@ await DeriveRecoveryKeyAsync(carryData.MasterKey, args.VersionedPassword) private void HandleEmitRecoveryKeyEvent(RecoveryKey recoveryKey) => _emitRecoveryKeyEventHandler?.Invoke(this, new EmitRecoveryKeyEventArgs(recoveryKey)); + private void HandlePrepareUserKeysBeginEvent() => + _prepareUserKeysBeginEventHandler?.Invoke(this, EventArgs.Empty); + + private void HandlePrepareUserKeysEndEvent() => + _prepareUserKeysEndEventHandler?.Invoke(this, EventArgs.Empty); + public event EventHandler EmitRecoveryKeyEventHandler { add => _emitRecoveryKeyEventHandler = @@ -93,6 +103,22 @@ public event EventHandler EmitRecoveryKeyEventHandler (EventHandler?)Delegate.Remove(_emitRecoveryKeyEventHandler, value); } + public event EventHandler PrepareUserKeysBeginEventHandler + { + add => _prepareUserKeysBeginEventHandler = + (EventHandler)Delegate.Combine(_prepareUserKeysBeginEventHandler, value); + remove => _prepareUserKeysBeginEventHandler = + (EventHandler?)Delegate.Remove(_prepareUserKeysBeginEventHandler, value); + } + + public event EventHandler PrepareUserKeysEndEventHandler + { + add => _prepareUserKeysEndEventHandler = + (EventHandler)Delegate.Combine(_prepareUserKeysEndEventHandler, value); + remove => _prepareUserKeysEndEventHandler = + (EventHandler?)Delegate.Remove(_prepareUserKeysEndEventHandler, value); + } + private void Recycle(object? _, EventArgs __) { MasterKey = Maybe.None; diff --git a/Crypter.Web/Client/Program.cs b/Crypter.Web/Client/Program.cs index 90f2a634e..6dafb8ec3 100644 --- a/Crypter.Web/Client/Program.cs +++ b/Crypter.Web/Client/Program.cs @@ -97,6 +97,7 @@ .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Crypter.Web/Shared/MainLayout.razor.cs b/Crypter.Web/Shared/MainLayout.razor.cs index bbf53a34d..d0786a173 100644 --- a/Crypter.Web/Shared/MainLayout.razor.cs +++ b/Crypter.Web/Shared/MainLayout.razor.cs @@ -71,6 +71,18 @@ protected override async Task OnInitializedAsync() ServicesInitialized = true; } + private void ShowUserKeysProgressModal(object? _, EventArgs __) + { + SpinnerModal.Open("Preparing Secret Keys", + "Please wait while your secret keys are being prepared.", + Maybe.None); + } + + private async void CloseUserKeysProgressModal(object? _, EventArgs __) + { + await SpinnerModal.CloseAsync(); + } + private void HandleRecoveryKeyCreatedEvent(object? _, EmitRecoveryKeyEventArgs args) { RecoveryKeyModal.Open(args.RecoveryKey); diff --git a/Crypter.Web/Shared/Modal/SpinnerModal.razor.cs b/Crypter.Web/Shared/Modal/SpinnerModal.razor.cs index 8587385c3..ae172dae1 100644 --- a/Crypter.Web/Shared/Modal/SpinnerModal.razor.cs +++ b/Crypter.Web/Shared/Modal/SpinnerModal.razor.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -24,7 +24,6 @@ * Contact the current copyright holder to discuss commercial license options. */ -using System; using System.Threading.Tasks; using Crypter.Web.Shared.Modal.Template; using EasyMonads; From 79b4316e1a154f48af82af3bfddc2b63a6acc368 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Jan 2025 11:50:30 -0600 Subject: [PATCH 10/23] MainLayout should actually subscribe to the new UserKeys events --- Crypter.Web/Shared/MainLayout.razor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Crypter.Web/Shared/MainLayout.razor.cs b/Crypter.Web/Shared/MainLayout.razor.cs index d0786a173..971623689 100644 --- a/Crypter.Web/Shared/MainLayout.razor.cs +++ b/Crypter.Web/Shared/MainLayout.razor.cs @@ -62,6 +62,8 @@ public class MainLayoutBase : LayoutComponentBase, IDisposable protected override async Task OnInitializedAsync() { EventfulUserKeysService.EmitRecoveryKeyEventHandler += HandleRecoveryKeyCreatedEvent; + EventfulUserKeysService.PrepareUserKeysBeginEventHandler += ShowUserKeysProgressModal; + EventfulUserKeysService.PrepareUserKeysEndEventHandler += CloseUserKeysProgressModal; UserPasswordService.PasswordHashBeginEventHandler += ShowPasswordHashingModal; UserPasswordService.PasswordHashEndEventHandler += ClosePasswordHashingModal; await BrowserRepository.InitializeAsync(); From e05e5a76aec9f0440213de57e3cd895c387bd483 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Jan 2025 13:02:58 -0600 Subject: [PATCH 11/23] IUserKeysService and IEventfulUserKeysService should resolve from the same instance --- .../Crypter.Common.Client.csproj | 4 + .../Services/EventfulUserKeysService.cs | 11 ++- .../Services/UserKeysService.cs | 77 +++++++++++++------ .../SubmitRecovery_Tests.cs | 5 +- Crypter.Web/Client/Program.cs | 10 ++- .../Transfer/UploadFileTransfer.razor.cs | 4 +- .../Transfer/UploadMessageTransfer.razor.cs | 18 +++-- .../Shared/Transfer/UploadTransferBase.cs | 7 +- .../wwwroot/appsettings.Development.json | 3 + Crypter.Web/wwwroot/appsettings.Staging.json | 15 ++-- Crypter.Web/wwwroot/appsettings.json | 7 +- 11 files changed, 103 insertions(+), 58 deletions(-) diff --git a/Crypter.Common.Client/Crypter.Common.Client.csproj b/Crypter.Common.Client/Crypter.Common.Client.csproj index 63a66ca97..88911a363 100644 --- a/Crypter.Common.Client/Crypter.Common.Client.csproj +++ b/Crypter.Common.Client/Crypter.Common.Client.csproj @@ -13,4 +13,8 @@ + + + + diff --git a/Crypter.Common.Client/Services/EventfulUserKeysService.cs b/Crypter.Common.Client/Services/EventfulUserKeysService.cs index 3c252015e..c36b4ab8e 100644 --- a/Crypter.Common.Client/Services/EventfulUserKeysService.cs +++ b/Crypter.Common.Client/Services/EventfulUserKeysService.cs @@ -32,19 +32,22 @@ using Crypter.Common.Client.Models; using Crypter.Crypto.Common; using EasyMonads; +using Microsoft.Extensions.Logging; namespace Crypter.Common.Client.Services; public sealed class EventfulUserKeysService : UserKeysService, IEventfulUserKeysService, IDisposable { + private readonly ILogger _logger; private readonly IUserSessionService _userSessionService; private EventHandler? _emitRecoveryKeyEventHandler; private event EventHandler? _prepareUserKeysBeginEventHandler; private event EventHandler? _prepareUserKeysEndEventHandler; - public EventfulUserKeysService(ICrypterApiClient crypterApiClient, ICryptoProvider cryptoProvider, IUserPasswordService userPasswordService, IUserKeysRepository userKeysRepository, IUserSessionService userSessionService) - : base(crypterApiClient, cryptoProvider, userPasswordService, userKeysRepository) + public EventfulUserKeysService(ILogger logger, ICrypterApiClient crypterApiClient, ICryptoProvider cryptoProvider, IUserPasswordService userPasswordService, IUserKeysRepository userKeysRepository, IUserSessionService userSessionService) + : base(logger, crypterApiClient, cryptoProvider, userPasswordService, userKeysRepository) { + _logger = logger; _userSessionService = userSessionService; _userSessionService.ServiceInitializedEventHandler += InitializeAsync; @@ -54,8 +57,10 @@ public EventfulUserKeysService(ICrypterApiClient crypterApiClient, ICryptoProvid private async void InitializeAsync(object? _, UserSessionServiceInitializedEventArgs args) { + _logger.LogDebug("Initializing"); if (args.IsLoggedIn) { + _logger.LogDebug($"Loading secret keys from {nameof(UserKeysRepository)}"); MasterKey = await UserKeysRepository.GetMasterKeyAsync(); PrivateKey = await UserKeysRepository.GetPrivateKeyAsync(); } @@ -121,12 +126,14 @@ public event EventHandler PrepareUserKeysEndEventHandler private void Recycle(object? _, EventArgs __) { + _logger.LogDebug("Recycling"); MasterKey = Maybe.None; PrivateKey = Maybe.None; } public void Dispose() { + _logger.LogDebug("Disposing"); _userSessionService.ServiceInitializedEventHandler -= InitializeAsync; _userSessionService.UserLoggedInEventHandler -= HandleUserLoginAsync; _userSessionService.UserLoggedOutEventHandler -= Recycle; diff --git a/Crypter.Common.Client/Services/UserKeysService.cs b/Crypter.Common.Client/Services/UserKeysService.cs index 6e383e27b..8095d77f0 100644 --- a/Crypter.Common.Client/Services/UserKeysService.cs +++ b/Crypter.Common.Client/Services/UserKeysService.cs @@ -35,24 +35,54 @@ using Crypter.Crypto.Common; using Crypter.Crypto.Common.KeyExchange; using EasyMonads; +using Microsoft.Extensions.Logging; namespace Crypter.Common.Client.Services; public class UserKeysService : IUserKeysService { - protected readonly ICrypterApiClient CrypterApiClient; - protected readonly ICryptoProvider CryptoProvider; + private readonly ILogger _logger; + private readonly ICrypterApiClient _crypterApiClient; + private readonly ICryptoProvider _cryptoProvider; protected readonly IUserPasswordService UserPasswordService; protected readonly IUserKeysRepository UserKeysRepository; - - public Maybe MasterKey { get; protected set; } = Maybe.None; - public Maybe PrivateKey { get; protected set; } = Maybe.None; - - public UserKeysService(ICrypterApiClient crypterApiClient, ICryptoProvider cryptoProvider, IUserPasswordService userPasswordService, IUserKeysRepository userKeysRepository) + private Maybe _masterKey = Maybe.None; + private Maybe _privateKey = Maybe.None; + + public Maybe MasterKey + { + get + { + _logger.LogDebug("Getting MasterKey. IsSome: {isSome}", _masterKey.IsSome); + return _masterKey; + } + protected set + { + _logger.LogDebug("Setting MasterKey. IsSome: {isSome}", value.IsSome); + _masterKey = value; + } + } + + public Maybe PrivateKey + { + get + { + _logger.LogDebug("Getting PrivateKey. IsSome: {isSome}", _privateKey.IsSome); + return _privateKey; + } + protected set + { + _logger.LogDebug("Setting PrivateKey. IsSome: {isSome}", value.IsSome); + _privateKey = value; + } + } + + public UserKeysService(ILogger logger, ICrypterApiClient crypterApiClient, ICryptoProvider cryptoProvider, IUserPasswordService userPasswordService, IUserKeysRepository userKeysRepository) { - CrypterApiClient = crypterApiClient; + _logger = logger; + _crypterApiClient = crypterApiClient; UserPasswordService = userPasswordService; - CryptoProvider = cryptoProvider; + _cryptoProvider = cryptoProvider; UserKeysRepository = userKeysRepository; } @@ -66,7 +96,7 @@ public Task> DeriveRecoveryKeyAsync(byte[] masterKey, Usernam public Task> DeriveRecoveryKeyAsync(byte[] masterKey, VersionedPassword versionedPassword) { GetMasterKeyRecoveryProofRequest request = new GetMasterKeyRecoveryProofRequest(versionedPassword.Password); - return CrypterApiClient.UserKey.GetMasterKeyRecoveryProofAsync(request) + return _crypterApiClient.UserKey.GetMasterKeyRecoveryProofAsync(request) .ToMaybeTask() .MapAsync(x => new RecoveryKey(masterKey, x.Proof)); } @@ -80,10 +110,10 @@ public Task> DeriveRecoveryKeyAsync(byte[] masterKey, Version /// protected async Task> GetOrCreateMasterKeyAsync(VersionedPassword versionedPassword, byte[] credentialKey) { - return await CrypterApiClient.UserKey.GetMasterKeyAsync() + return await _crypterApiClient.UserKey.GetMasterKeyAsync() .BindAsync(x => { - byte[] decryptedMasterKey = CryptoProvider.Encryption.Decrypt(credentialKey, x.Nonce, x.EncryptedKey); + byte[] decryptedMasterKey = _cryptoProvider.Encryption.Decrypt(credentialKey, x.Nonce, x.EncryptedKey); return new GetOrCreateMasterKeyResult(decryptedMasterKey, Maybe.None); }) .MatchAsync( @@ -107,10 +137,10 @@ protected async Task> GetOrCreateMasterKeyAsyn /// protected async Task> GetOrCreateKeyPairAsync(byte[] masterKey) { - return await CrypterApiClient.UserKey.GetPrivateKeyAsync() + return await _crypterApiClient.UserKey.GetPrivateKeyAsync() .BindAsync(x => { - byte[] decryptedPrivateKey = CryptoProvider.Encryption.Decrypt(masterKey, x.Nonce, x.EncryptedKey); + byte[] decryptedPrivateKey = _cryptoProvider.Encryption.Decrypt(masterKey, x.Nonce, x.EncryptedKey); return new GetOrCreateKeyPairResult(decryptedPrivateKey); }) .MatchAsync( @@ -128,31 +158,32 @@ protected async Task> GetOrCreateKeyPairAsync(by private Task> UploadNewMasterKeyAsync(VersionedPassword versionedPassword, byte[] credentialKey) { - byte[] newMasterKey = CryptoProvider.Random.GenerateRandomBytes((int)CryptoProvider.Encryption.KeySize); - byte[] nonce = CryptoProvider.Random.GenerateRandomBytes((int)CryptoProvider.Encryption.NonceSize); - byte[] encryptedMasterKey = CryptoProvider.Encryption.Encrypt(credentialKey, nonce, newMasterKey); - byte[] recoveryProof = CryptoProvider.Random.GenerateRandomBytes(32); + byte[] newMasterKey = _cryptoProvider.Random.GenerateRandomBytes((int)_cryptoProvider.Encryption.KeySize); + byte[] nonce = _cryptoProvider.Random.GenerateRandomBytes((int)_cryptoProvider.Encryption.NonceSize); + byte[] encryptedMasterKey = _cryptoProvider.Encryption.Encrypt(credentialKey, nonce, newMasterKey); + byte[] recoveryProof = _cryptoProvider.Random.GenerateRandomBytes(32); InsertMasterKeyRequest request = new InsertMasterKeyRequest(versionedPassword.Password, encryptedMasterKey, nonce, recoveryProof); - return CrypterApiClient.UserKey.InsertMasterKeyAsync(request) + return _crypterApiClient.UserKey.InsertMasterKeyAsync(request) .ToMaybeTask() .BindAsync(_ => new GetOrCreateMasterKeyResult(newMasterKey, new RecoveryKey(newMasterKey, request.RecoveryProof))); } private Task> UploadNewUserKeyPairAsync(byte[] masterKey) { - X25519KeyPair keyPair = CryptoProvider.KeyExchange.GenerateKeyPair(); - byte[] nonce = CryptoProvider.Random.GenerateRandomBytes((int)CryptoProvider.Encryption.NonceSize); - byte[] encryptedPrivateKey = CryptoProvider.Encryption.Encrypt(masterKey, nonce, keyPair.PrivateKey); + X25519KeyPair keyPair = _cryptoProvider.KeyExchange.GenerateKeyPair(); + byte[] nonce = _cryptoProvider.Random.GenerateRandomBytes((int)_cryptoProvider.Encryption.NonceSize); + byte[] encryptedPrivateKey = _cryptoProvider.Encryption.Encrypt(masterKey, nonce, keyPair.PrivateKey); InsertKeyPairRequest request = new InsertKeyPairRequest(encryptedPrivateKey, keyPair.PublicKey, nonce); - return CrypterApiClient.UserKey.InsertKeyPairAsync(request) + return _crypterApiClient.UserKey.InsertKeyPairAsync(request) .ToMaybeTask() .BindAsync(_ => new GetOrCreateKeyPairResult(keyPair.PrivateKey)); } protected async Task StoreSecretKeysAsync(byte[] masterKey, byte[] privateKey, bool trustDevice) { + _logger.LogDebug("Storing secret keys"); MasterKey = masterKey; PrivateKey = privateKey; await UserKeysRepository.StoreMasterKeyAsync(masterKey, trustDevice); diff --git a/Crypter.Test/Integration_Tests/UserRecovery_Tests/SubmitRecovery_Tests.cs b/Crypter.Test/Integration_Tests/UserRecovery_Tests/SubmitRecovery_Tests.cs index 90954458c..056863d47 100644 --- a/Crypter.Test/Integration_Tests/UserRecovery_Tests/SubmitRecovery_Tests.cs +++ b/Crypter.Test/Integration_Tests/UserRecovery_Tests/SubmitRecovery_Tests.cs @@ -49,6 +49,7 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; namespace Crypter.Test.Integration_Tests.UserRecovery_Tests; @@ -116,7 +117,7 @@ await loginResult.DoRightAsync(async loginResponse => UserPasswordService userPasswordService = new UserPasswordService(_cryptoProvider!); IUserKeysRepository userKeysRepository = new MemoryKeysRepository(); - UserKeysService userKeysService = new UserKeysService(_client, new DefaultCryptoProvider(), userPasswordService, userKeysRepository); + UserKeysService userKeysService = new UserKeysService(NullLogger.Instance, _client, new DefaultCryptoProvider(), userPasswordService, userKeysRepository); recoveryKey = await userKeysService.DeriveRecoveryKeyAsync(masterKey, registrationRequest.VersionedPassword); recoveryKey.IfNone(Assert.Fail); } @@ -209,7 +210,7 @@ await loginResult.DoRightAsync(async loginResponse => UserPasswordService userPasswordService = new UserPasswordService(_cryptoProvider!); IUserKeysRepository userKeysRepository = new MemoryKeysRepository(); - UserKeysService userKeysService = new UserKeysService(_client, new DefaultCryptoProvider(), userPasswordService, userKeysRepository); + UserKeysService userKeysService = new UserKeysService(NullLogger.Instance, _client, new DefaultCryptoProvider(), userPasswordService, userKeysRepository); Maybe recoveryKeyResponse = await userKeysService.DeriveRecoveryKeyAsync(masterKey, registrationRequest.VersionedPassword); await recoveryKeyResponse diff --git a/Crypter.Web/Client/Program.cs b/Crypter.Web/Client/Program.cs index 6dafb8ec3..91eba3875 100644 --- a/Crypter.Web/Client/Program.cs +++ b/Crypter.Web/Client/Program.cs @@ -49,12 +49,14 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; WebAssemblyHostBuilder builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); builder.RootComponents.Add("head::after"); Console.WriteLine($"Environment: {builder.HostEnvironment.Environment}"); +builder.Logging.SetMinimumLevel(builder.Configuration.GetValue("LoggingConfiguration:MinimumLogLevel") ?? LogLevel.Information); builder.Services.AddSingleton(sp => { @@ -97,15 +99,17 @@ .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton>(sp => sp.GetRequiredService); + .AddSingleton>(sp => sp.GetRequiredService) + + .AddSingleton() + .AddSingleton(x => x.GetRequiredService()) + .AddSingleton(x => x.GetRequiredService()); if (OperatingSystem.IsBrowser()) { diff --git a/Crypter.Web/Shared/Transfer/UploadFileTransfer.razor.cs b/Crypter.Web/Shared/Transfer/UploadFileTransfer.razor.cs index 448fe08c1..a984801fa 100644 --- a/Crypter.Web/Shared/Transfer/UploadFileTransfer.razor.cs +++ b/Crypter.Web/Shared/Transfer/UploadFileTransfer.razor.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -138,7 +138,7 @@ private async Task OnEncryptClicked() await Task.Delay(400); } - await HandleUploadResponse(uploadResponse); + await HandleUploadResponseAsync(uploadResponse); Dispose(); return; diff --git a/Crypter.Web/Shared/Transfer/UploadMessageTransfer.razor.cs b/Crypter.Web/Shared/Transfer/UploadMessageTransfer.razor.cs index bc5a4f79e..8b944f61b 100644 --- a/Crypter.Web/Shared/Transfer/UploadMessageTransfer.razor.cs +++ b/Crypter.Web/Shared/Transfer/UploadMessageTransfer.razor.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -25,7 +25,12 @@ */ using System; +using System.Reflection.Metadata; using System.Threading.Tasks; +using Crypter.Common.Client.Transfer.Handlers; +using Crypter.Common.Client.Transfer.Models; +using Crypter.Common.Contracts.Features.Transfer; +using EasyMonads; namespace Crypter.Web.Shared.Transfer; @@ -40,17 +45,16 @@ protected async Task OnEncryptClicked() EncryptionInProgress = true; ErrorMessage = string.Empty; - await SetProgressMessage("Encrypting message"); - var messageUploader = - TransferHandlerFactory.CreateUploadMessageHandler(MessageSubject, MessageBody, ExpirationHours); + await SetProgressMessageAsync("Encrypting message"); + UploadMessageHandler messageUploader = TransferHandlerFactory.CreateUploadMessageHandler(MessageSubject, MessageBody, ExpirationHours); SetHandlerUserInfo(messageUploader); - var uploadResponse = await messageUploader.UploadAsync(); - await HandleUploadResponse(uploadResponse); + Either uploadResponse = await messageUploader.UploadAsync(); + await HandleUploadResponseAsync(uploadResponse); Dispose(); } - protected async Task SetProgressMessage(string message) + protected async Task SetProgressMessageAsync(string message) { UploadStatusMessage = message; StateHasChanged(); diff --git a/Crypter.Web/Shared/Transfer/UploadTransferBase.cs b/Crypter.Web/Shared/Transfer/UploadTransferBase.cs index f958a7849..68a66c186 100644 --- a/Crypter.Web/Shared/Transfer/UploadTransferBase.cs +++ b/Crypter.Web/Shared/Transfer/UploadTransferBase.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -33,7 +33,6 @@ using Crypter.Common.Client.Transfer.Models; using Crypter.Common.Contracts.Features.Transfer; using Crypter.Common.Enums; -using Crypter.Web.Services; using Crypter.Web.Shared.Modal; using EasyMonads; using Microsoft.AspNetCore.Components; @@ -53,8 +52,6 @@ public class UploadTransferBase : ComponentBase [Inject] protected TransferHandlerFactory TransferHandlerFactory { get; init; } = null!; - [Inject] protected IFileSaverService FileSaverService { get; init; } = null!; - [Parameter] public Maybe RecipientUsername { get; set; } [Parameter] public Maybe RecipientPublicKey { get; set; } @@ -99,7 +96,7 @@ protected void SetHandlerUserInfo(IUserUploadHandler handler) }); } - protected async Task HandleUploadResponse(Either uploadResponse) + protected async Task HandleUploadResponseAsync(Either uploadResponse) { await uploadResponse .DoRightAsync(async response => diff --git a/Crypter.Web/wwwroot/appsettings.Development.json b/Crypter.Web/wwwroot/appsettings.Development.json index 8a4099c69..f94ab870b 100644 --- a/Crypter.Web/wwwroot/appsettings.Development.json +++ b/Crypter.Web/wwwroot/appsettings.Development.json @@ -1,5 +1,8 @@ { "ApiSettings": { "ApiBaseUrl": "https://localhost/api" + }, + "LoggingConfiguration": { + "MinimumLogLevel": "Debug" } } diff --git a/Crypter.Web/wwwroot/appsettings.Staging.json b/Crypter.Web/wwwroot/appsettings.Staging.json index 703718263..efd2d29ae 100644 --- a/Crypter.Web/wwwroot/appsettings.Staging.json +++ b/Crypter.Web/wwwroot/appsettings.Staging.json @@ -1,11 +1,8 @@ { - "ApiSettings": { - "ApiBaseUrl": "${API_BASE_URL}" - }, - "Logging": { - "LogLevel": { - "Default": "Debug", - "Microsoft.AspNetCore": "Information" - } - } + "ApiSettings": { + "ApiBaseUrl": "${API_BASE_URL}" + }, + "LoggingConfiguration": { + "MinimumLogLevel": "Debug" + } } diff --git a/Crypter.Web/wwwroot/appsettings.json b/Crypter.Web/wwwroot/appsettings.json index f7e7e9c50..89380fcf4 100644 --- a/Crypter.Web/wwwroot/appsettings.json +++ b/Crypter.Web/wwwroot/appsettings.json @@ -13,10 +13,7 @@ "MaxReadSize": 32704, "PadSize": 64 }, - "Logging": { - "LogLevel": { - "Default": "Warning", - "Microsoft.AspNetCore": "Warning" - } + "LoggingConfiguration": { + "MinimumLogLevel": "Information" } } From 44d46158ee654018ec4c563281b0f7edd46ddc03 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Mon, 13 Jan 2025 21:13:30 -0600 Subject: [PATCH 12/23] Move AddUserContactError.cs and thus eliminate a namespace --- .../Controllers/UserContactController.cs | 1 - .../Requests/UserContactRequests.cs | 1 - .../Requests/IUserContactRequests.cs | 1 - .../Services/IUserContactsService.cs | 1 - .../Services/UserContactsService.cs | 1 - .../{ErrorCodes => }/AddUserContactError.cs | 68 +++++++++---------- .../Commands/AddUserContactCommand.cs | 1 - .../UserContact_Tests/AddUserContact_Tests.cs | 1 - .../UserContact_Tests/GetUserContact_Tests.cs | 1 - .../RemoveUserContact_Tests.cs | 1 - 10 files changed, 34 insertions(+), 43 deletions(-) rename Crypter.Common/Contracts/Features/Contacts/{ErrorCodes => }/AddUserContactError.cs (92%) diff --git a/Crypter.API/Controllers/UserContactController.cs b/Crypter.API/Controllers/UserContactController.cs index 6ce5bfde1..269edad56 100644 --- a/Crypter.API/Controllers/UserContactController.cs +++ b/Crypter.API/Controllers/UserContactController.cs @@ -31,7 +31,6 @@ using Crypter.API.Controllers.Base; using Crypter.Common.Contracts; using Crypter.Common.Contracts.Features.Contacts; -using Crypter.Common.Contracts.Features.Contacts.RequestErrorCodes; using Crypter.Core.Features.UserContacts.Commands; using Crypter.Core.Features.UserContacts.Queries; using EasyMonads; diff --git a/Crypter.Common.Client/HttpClients/Requests/UserContactRequests.cs b/Crypter.Common.Client/HttpClients/Requests/UserContactRequests.cs index 4b79fec30..bc387b301 100644 --- a/Crypter.Common.Client/HttpClients/Requests/UserContactRequests.cs +++ b/Crypter.Common.Client/HttpClients/Requests/UserContactRequests.cs @@ -29,7 +29,6 @@ using Crypter.Common.Client.Interfaces.HttpClients; using Crypter.Common.Client.Interfaces.Requests; using Crypter.Common.Contracts.Features.Contacts; -using Crypter.Common.Contracts.Features.Contacts.RequestErrorCodes; using EasyMonads; namespace Crypter.Common.Client.HttpClients.Requests; diff --git a/Crypter.Common.Client/Interfaces/Requests/IUserContactRequests.cs b/Crypter.Common.Client/Interfaces/Requests/IUserContactRequests.cs index c26d26d9b..fee8cc5ae 100644 --- a/Crypter.Common.Client/Interfaces/Requests/IUserContactRequests.cs +++ b/Crypter.Common.Client/Interfaces/Requests/IUserContactRequests.cs @@ -27,7 +27,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using Crypter.Common.Contracts.Features.Contacts; -using Crypter.Common.Contracts.Features.Contacts.RequestErrorCodes; using EasyMonads; namespace Crypter.Common.Client.Interfaces.Requests; diff --git a/Crypter.Common.Client/Interfaces/Services/IUserContactsService.cs b/Crypter.Common.Client/Interfaces/Services/IUserContactsService.cs index 5401a2064..d8efba6fb 100644 --- a/Crypter.Common.Client/Interfaces/Services/IUserContactsService.cs +++ b/Crypter.Common.Client/Interfaces/Services/IUserContactsService.cs @@ -27,7 +27,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using Crypter.Common.Contracts.Features.Contacts; -using Crypter.Common.Contracts.Features.Contacts.RequestErrorCodes; using EasyMonads; namespace Crypter.Common.Client.Interfaces.Services; diff --git a/Crypter.Common.Client/Services/UserContactsService.cs b/Crypter.Common.Client/Services/UserContactsService.cs index ea0ef7393..3f87de5ed 100644 --- a/Crypter.Common.Client/Services/UserContactsService.cs +++ b/Crypter.Common.Client/Services/UserContactsService.cs @@ -33,7 +33,6 @@ using Crypter.Common.Client.Interfaces.HttpClients; using Crypter.Common.Client.Interfaces.Services; using Crypter.Common.Contracts.Features.Contacts; -using Crypter.Common.Contracts.Features.Contacts.RequestErrorCodes; using EasyMonads; namespace Crypter.Common.Client.Services; diff --git a/Crypter.Common/Contracts/Features/Contacts/ErrorCodes/AddUserContactError.cs b/Crypter.Common/Contracts/Features/Contacts/AddUserContactError.cs similarity index 92% rename from Crypter.Common/Contracts/Features/Contacts/ErrorCodes/AddUserContactError.cs rename to Crypter.Common/Contracts/Features/Contacts/AddUserContactError.cs index e10f9ad99..bf2bdd150 100644 --- a/Crypter.Common/Contracts/Features/Contacts/ErrorCodes/AddUserContactError.cs +++ b/Crypter.Common/Contracts/Features/Contacts/AddUserContactError.cs @@ -1,34 +1,34 @@ -/* - * Copyright (C) 2023 Crypter File Transfer - * - * This file is part of the Crypter file transfer project. - * - * Crypter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * The Crypter source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * You can be released from the requirements of the aforementioned license - * by purchasing a commercial license. Buying such a license is mandatory - * as soon as you develop commercial activities involving the Crypter source - * code without disclosing the source code of your own applications. - * - * Contact the current copyright holder to discuss commercial license options. - */ - -namespace Crypter.Common.Contracts.Features.Contacts.RequestErrorCodes; - -public enum AddUserContactError -{ - UnknownError, - NotFound, - InvalidUser -} +/* + * Copyright (C) 2023 Crypter File Transfer + * + * This file is part of the Crypter file transfer project. + * + * Crypter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Crypter source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the aforementioned license + * by purchasing a commercial license. Buying such a license is mandatory + * as soon as you develop commercial activities involving the Crypter source + * code without disclosing the source code of your own applications. + * + * Contact the current copyright holder to discuss commercial license options. + */ + +namespace Crypter.Common.Contracts.Features.Contacts; + +public enum AddUserContactError +{ + UnknownError, + NotFound, + InvalidUser +} diff --git a/Crypter.Core/Features/UserContacts/Commands/AddUserContactCommand.cs b/Crypter.Core/Features/UserContacts/Commands/AddUserContactCommand.cs index 9733ad670..50044e370 100644 --- a/Crypter.Core/Features/UserContacts/Commands/AddUserContactCommand.cs +++ b/Crypter.Core/Features/UserContacts/Commands/AddUserContactCommand.cs @@ -29,7 +29,6 @@ using System.Threading; using System.Threading.Tasks; using Crypter.Common.Contracts.Features.Contacts; -using Crypter.Common.Contracts.Features.Contacts.RequestErrorCodes; using Crypter.Core.LinqExpressions; using Crypter.Core.MediatorMonads; using Crypter.DataAccess; diff --git a/Crypter.Test/Integration_Tests/UserContact_Tests/AddUserContact_Tests.cs b/Crypter.Test/Integration_Tests/UserContact_Tests/AddUserContact_Tests.cs index 63c1b5077..7b969be79 100644 --- a/Crypter.Test/Integration_Tests/UserContact_Tests/AddUserContact_Tests.cs +++ b/Crypter.Test/Integration_Tests/UserContact_Tests/AddUserContact_Tests.cs @@ -28,7 +28,6 @@ using Crypter.Common.Client.Interfaces.HttpClients; using Crypter.Common.Client.Interfaces.Repositories; using Crypter.Common.Contracts.Features.Contacts; -using Crypter.Common.Contracts.Features.Contacts.RequestErrorCodes; using Crypter.Common.Contracts.Features.UserAuthentication; using Crypter.Common.Enums; using EasyMonads; diff --git a/Crypter.Test/Integration_Tests/UserContact_Tests/GetUserContact_Tests.cs b/Crypter.Test/Integration_Tests/UserContact_Tests/GetUserContact_Tests.cs index 1d9ec6b4b..1e8da2137 100644 --- a/Crypter.Test/Integration_Tests/UserContact_Tests/GetUserContact_Tests.cs +++ b/Crypter.Test/Integration_Tests/UserContact_Tests/GetUserContact_Tests.cs @@ -29,7 +29,6 @@ using Crypter.Common.Client.Interfaces.HttpClients; using Crypter.Common.Client.Interfaces.Repositories; using Crypter.Common.Contracts.Features.Contacts; -using Crypter.Common.Contracts.Features.Contacts.RequestErrorCodes; using Crypter.Common.Contracts.Features.UserAuthentication; using Crypter.Common.Enums; using EasyMonads; diff --git a/Crypter.Test/Integration_Tests/UserContact_Tests/RemoveUserContact_Tests.cs b/Crypter.Test/Integration_Tests/UserContact_Tests/RemoveUserContact_Tests.cs index 3e6fa18a5..e08372ad4 100644 --- a/Crypter.Test/Integration_Tests/UserContact_Tests/RemoveUserContact_Tests.cs +++ b/Crypter.Test/Integration_Tests/UserContact_Tests/RemoveUserContact_Tests.cs @@ -29,7 +29,6 @@ using Crypter.Common.Client.Interfaces.HttpClients; using Crypter.Common.Client.Interfaces.Repositories; using Crypter.Common.Contracts.Features.Contacts; -using Crypter.Common.Contracts.Features.Contacts.RequestErrorCodes; using Crypter.Common.Contracts.Features.UserAuthentication; using Crypter.Common.Enums; using EasyMonads; From bf2806da3d8d42c410af9c9243b926d7418b1ab1 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Mon, 13 Jan 2025 22:55:40 -0600 Subject: [PATCH 13/23] Decouple recovery key consent handling from the login response model --- Crypter.API/Controllers/ConsentController.cs | 24 +- .../CrypterAuthenticatedHttpClient.cs | 10 + .../HttpClients/CrypterHttpClient.cs | 10 + .../Requests/UserConsentRequests.cs | 18 +- .../Requests/UserSettingRequests.cs | 20 +- .../HttpClients/ICrypterHttpClient.cs | 5 +- .../Requests/IUserConsentRequests.cs | 8 +- .../Services/UserSessionService.cs | 14 +- .../UserAuthentication/Login/LoginResponse.cs | 4 +- .../UserConsents/UserConsentRequest.cs | 40 + .../Features/UserConsents/UserConsentType.cs | 35 + .../Commands/SubmitAccountRecoveryCommand.cs | 7 +- .../Commands/UserLoginCommand.cs | 12 +- ...cknowledgementOfRecoveryKeyRisksCommand.cs | 31 - .../Commands/SaveUserConsentCommand.cs | 32 + .../Queries/GetUserConsentsQuery.cs | 71 ++ .../Entities/UserConsentEntity.cs | 19 +- ...045437_RenameUserConsentColumn.Designer.cs | 737 ++++++++++++++++++ .../20250114045437_RenameUserConsentColumn.cs | 30 + .../Migrations/DataContextModelSnapshot.cs | 8 +- .../RecoveryKeyRisksConsent_Tests.cs | 6 +- .../Shared/Modal/RecoveryKeyModal.razor.cs | 3 +- 22 files changed, 1051 insertions(+), 93 deletions(-) create mode 100644 Crypter.Common/Contracts/Features/UserConsents/UserConsentRequest.cs create mode 100644 Crypter.Common/Contracts/Features/UserConsents/UserConsentType.cs delete mode 100644 Crypter.Core/Features/UserConsent/Commands/SaveAcknowledgementOfRecoveryKeyRisksCommand.cs create mode 100644 Crypter.Core/Features/UserConsent/Commands/SaveUserConsentCommand.cs create mode 100644 Crypter.Core/Features/UserConsent/Queries/GetUserConsentsQuery.cs create mode 100644 Crypter.DataAccess/Migrations/20250114045437_RenameUserConsentColumn.Designer.cs create mode 100644 Crypter.DataAccess/Migrations/20250114045437_RenameUserConsentColumn.cs diff --git a/Crypter.API/Controllers/ConsentController.cs b/Crypter.API/Controllers/ConsentController.cs index dbe805f18..b06a71262 100644 --- a/Crypter.API/Controllers/ConsentController.cs +++ b/Crypter.API/Controllers/ConsentController.cs @@ -24,9 +24,14 @@ * Contact the current copyright holder to discuss commercial license options. */ +using System; +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Crypter.API.Controllers.Base; +using Crypter.Common.Contracts.Features.UserConsents; using Crypter.Core.Features.UserConsent.Commands; +using Crypter.Core.Features.UserConsent.Queries; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -45,14 +50,25 @@ public ConsentController(ISender sender) _sender = sender; } - [HttpPost("recovery-key-risk")] + [HttpGet] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Dictionary))] + [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(void))] + public async Task GetUserConsentsAsync(CancellationToken cancellationToken) + { + GetUserConsentsQuery query = new GetUserConsentsQuery(UserId); + Dictionary result = await _sender.Send(query, cancellationToken); + return Ok(result); + } + + [HttpPost] [Authorize] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(void))] [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(void))] - public async Task ConsentToRecoveryKeyRisksAsync() + public async Task ConsentAsync(UserConsentRequest request) { - SaveAcknowledgementOfRecoveryKeyRisksCommand request = new SaveAcknowledgementOfRecoveryKeyRisksCommand(UserId); - await _sender.Send(request); + SaveUserConsentCommand command = new SaveUserConsentCommand(UserId, request.ConsentType); + await _sender.Send(command, CancellationToken.None); return Ok(); } } diff --git a/Crypter.Common.Client/HttpClients/CrypterAuthenticatedHttpClient.cs b/Crypter.Common.Client/HttpClients/CrypterAuthenticatedHttpClient.cs index 5e527ce44..630ca893a 100644 --- a/Crypter.Common.Client/HttpClients/CrypterAuthenticatedHttpClient.cs +++ b/Crypter.Common.Client/HttpClients/CrypterAuthenticatedHttpClient.cs @@ -154,6 +154,16 @@ public async Task> PostEitherAsync(response); } + public async Task> PostMaybeUnitResponseAsync(string uri, TRequest body) + where TRequest : class + { + Func requestFactory = MakeRequestMessageFactory(HttpMethod.Post, uri, body); + using HttpResponseMessage response = await SendWithAuthenticationAsync(requestFactory, false); + return response.IsSuccessStatusCode + ? Unit.Default + : Maybe.None; + } + public async Task> PostMaybeUnitResponseAsync(string uri) { Func requestFactory = MakeRequestMessageFactory(HttpMethod.Post, uri); diff --git a/Crypter.Common.Client/HttpClients/CrypterHttpClient.cs b/Crypter.Common.Client/HttpClients/CrypterHttpClient.cs index 3fefe97f1..0882d3f41 100644 --- a/Crypter.Common.Client/HttpClients/CrypterHttpClient.cs +++ b/Crypter.Common.Client/HttpClients/CrypterHttpClient.cs @@ -84,6 +84,16 @@ public async Task> PostEitherAsync(request); } + public async Task> PostMaybeUnitResponseAsync(string uri, TRequest body) + where TRequest : class + { + using HttpRequestMessage request = MakeRequestMessage(HttpMethod.Post, uri, body); + using HttpResponseMessage response = await _httpClient.SendAsync(request); + return response.IsSuccessStatusCode + ? Unit.Default + : Maybe.None; + } + public async Task> PostMaybeUnitResponseAsync(string uri) { using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri); diff --git a/Crypter.Common.Client/HttpClients/Requests/UserConsentRequests.cs b/Crypter.Common.Client/HttpClients/Requests/UserConsentRequests.cs index 58cc9fc28..dde08a2c6 100644 --- a/Crypter.Common.Client/HttpClients/Requests/UserConsentRequests.cs +++ b/Crypter.Common.Client/HttpClients/Requests/UserConsentRequests.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -24,9 +24,12 @@ * Contact the current copyright holder to discuss commercial license options. */ +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Crypter.Common.Client.Interfaces.HttpClients; using Crypter.Common.Client.Interfaces.Requests; +using Crypter.Common.Contracts.Features.UserConsents; using EasyMonads; namespace Crypter.Common.Client.HttpClients.Requests; @@ -40,9 +43,16 @@ public UserConsentRequests(ICrypterAuthenticatedHttpClient crypterAuthenticatedH _crypterAuthenticatedHttpClient = crypterAuthenticatedHttpClient; } - public Task> ConsentToRecoveryKeyRisksAsync() + public Task>> GetUserConsentsAsync() { - const string url = "api/user/consent/recovery-key-risk"; - return _crypterAuthenticatedHttpClient.PostMaybeUnitResponseAsync(url); + const string url = "api/user/consent"; + return _crypterAuthenticatedHttpClient.GetMaybeAsync>(url); + } + + public Task> ConsentAsync(UserConsentType consentType) + { + const string url = "api/user/consent"; + UserConsentRequest request = new UserConsentRequest(consentType); + return _crypterAuthenticatedHttpClient.PostMaybeUnitResponseAsync(url, request); } } diff --git a/Crypter.Common.Client/HttpClients/Requests/UserSettingRequests.cs b/Crypter.Common.Client/HttpClients/Requests/UserSettingRequests.cs index c21753ee9..f129c8dc2 100644 --- a/Crypter.Common.Client/HttpClients/Requests/UserSettingRequests.cs +++ b/Crypter.Common.Client/HttpClients/Requests/UserSettingRequests.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -41,8 +41,7 @@ public class UserSettingRequests : IUserSettingRequests private readonly ICrypterHttpClient _crypterHttpClient; private readonly ICrypterAuthenticatedHttpClient _crypterAuthenticatedHttpClient; - public UserSettingRequests(ICrypterHttpClient crypterHttpClient, - ICrypterAuthenticatedHttpClient crypterAuthenticatedHttpClient) + public UserSettingRequests(ICrypterHttpClient crypterHttpClient, ICrypterAuthenticatedHttpClient crypterAuthenticatedHttpClient) { _crypterHttpClient = crypterHttpClient; _crypterAuthenticatedHttpClient = crypterAuthenticatedHttpClient; @@ -54,8 +53,7 @@ public Task> GetProfileSettingsAsync() return _crypterAuthenticatedHttpClient.GetMaybeAsync(url); } - public Task> SetProfileSettingsAsync( - ProfileSettings newProfileSettings) + public Task> SetProfileSettingsAsync(ProfileSettings newProfileSettings) { const string url = "api/user/setting/profile"; return _crypterAuthenticatedHttpClient.PutEitherAsync(url, newProfileSettings) @@ -68,8 +66,7 @@ public Task> GetContactInfoSettingsAsync() return _crypterAuthenticatedHttpClient.GetMaybeAsync(url); } - public Task> UpdateContactInfoSettingsAsync( - UpdateContactInfoSettingsRequest newContactInfoSettings) + public Task> UpdateContactInfoSettingsAsync(UpdateContactInfoSettingsRequest newContactInfoSettings) { const string url = "api/user/setting/contact"; return _crypterAuthenticatedHttpClient @@ -83,8 +80,7 @@ public Task> GetNotificationSettingsAsync() return _crypterAuthenticatedHttpClient.GetMaybeAsync(url); } - public Task> UpdateNotificationSettingsAsync( - NotificationSettings newNotificationSettings) + public Task> UpdateNotificationSettingsAsync(NotificationSettings newNotificationSettings) { const string url = "api/user/setting/notification"; return _crypterAuthenticatedHttpClient @@ -98,16 +94,14 @@ public Task> GetPrivacySettingsAsync() return _crypterAuthenticatedHttpClient.GetMaybeAsync(url); } - public Task> SetPrivacySettingsAsync( - PrivacySettings newPrivacySettings) + public Task> SetPrivacySettingsAsync(PrivacySettings newPrivacySettings) { const string url = "api/user/setting/privacy"; return _crypterAuthenticatedHttpClient.PutEitherAsync(url, newPrivacySettings) .ExtractErrorCode(); } - public Task> VerifyUserEmailAddressAsync( - VerifyEmailAddressRequest verificationInfo) + public Task> VerifyUserEmailAddressAsync(VerifyEmailAddressRequest verificationInfo) { const string url = "api/user/setting/contact/verify"; return _crypterHttpClient.PostEitherUnitResponseAsync(url, verificationInfo) diff --git a/Crypter.Common.Client/Interfaces/HttpClients/ICrypterHttpClient.cs b/Crypter.Common.Client/Interfaces/HttpClients/ICrypterHttpClient.cs index 50ec57ae7..83958076d 100644 --- a/Crypter.Common.Client/Interfaces/HttpClients/ICrypterHttpClient.cs +++ b/Crypter.Common.Client/Interfaces/HttpClients/ICrypterHttpClient.cs @@ -47,9 +47,12 @@ Task> GetEitherAsync(string uri) Task> PostEitherAsync(string uri, TRequest body) where TRequest : class where TResponse : class; - + Task> PostMaybeUnitResponseAsync(string uri); + Task> PostMaybeUnitResponseAsync(string uri, TRequest body) + where TRequest : class; + Task> PostEitherUnitResponseAsync(string uri); Task> PostEitherUnitResponseAsync(string uri, TRequest body) diff --git a/Crypter.Common.Client/Interfaces/Requests/IUserConsentRequests.cs b/Crypter.Common.Client/Interfaces/Requests/IUserConsentRequests.cs index f22711826..32d87e344 100644 --- a/Crypter.Common.Client/Interfaces/Requests/IUserConsentRequests.cs +++ b/Crypter.Common.Client/Interfaces/Requests/IUserConsentRequests.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -24,12 +24,16 @@ * Contact the current copyright holder to discuss commercial license options. */ +using System; +using System.Collections.Generic; using System.Threading.Tasks; +using Crypter.Common.Contracts.Features.UserConsents; using EasyMonads; namespace Crypter.Common.Client.Interfaces.Requests; public interface IUserConsentRequests { - Task> ConsentToRecoveryKeyRisksAsync(); + Task>> GetUserConsentsAsync(); + Task> ConsentAsync(UserConsentType consentType); } diff --git a/Crypter.Common.Client/Services/UserSessionService.cs b/Crypter.Common.Client/Services/UserSessionService.cs index 7038be77e..9a6269a1f 100644 --- a/Crypter.Common.Client/Services/UserSessionService.cs +++ b/Crypter.Common.Client/Services/UserSessionService.cs @@ -35,6 +35,7 @@ using Crypter.Common.Client.Interfaces.Services; using Crypter.Common.Client.Models; using Crypter.Common.Contracts.Features.UserAuthentication; +using Crypter.Common.Contracts.Features.UserConsents; using Crypter.Common.Enums; using Crypter.Common.Primitives; using EasyMonads; @@ -144,9 +145,16 @@ public Task> LoginAsync(Username username, Password pas from unit0 in Either.FromRightAsync(StoreSessionInfo(loginResponse, rememberUser)) select loginResponse; - Either loginResult = await loginTask; - loginResult.DoRight(x => HandleUserLoggedInEvent(username, password, versionedPassword, rememberUser, x.ShowRecoveryKey)); - return loginResult.Map(_ => Unit.Default); + return await loginTask + .DoRightAsync(async x => + { + bool showRecoveryKeyModal = await _crypterApiClient.UserConsent.GetUserConsentsAsync() + .MatchAsync( + none: () => false, + some: y => y.TryGetValue(UserConsentType.RecoveryKeyRisks, out DateTimeOffset? value) && !value.HasValue); + HandleUserLoggedInEvent(username, password, versionedPassword, rememberUser, showRecoveryKeyModal); + }) + .BindAsync(_ => Unit.Default); }); } diff --git a/Crypter.Common/Contracts/Features/UserAuthentication/Login/LoginResponse.cs b/Crypter.Common/Contracts/Features/UserAuthentication/Login/LoginResponse.cs index b9de26cd6..dc2601a39 100644 --- a/Crypter.Common/Contracts/Features/UserAuthentication/Login/LoginResponse.cs +++ b/Crypter.Common/Contracts/Features/UserAuthentication/Login/LoginResponse.cs @@ -33,14 +33,12 @@ public class LoginResponse public string Username { get; init; } public string AuthenticationToken { get; init; } public string RefreshToken { get; init; } - public bool ShowRecoveryKey { get; init; } [JsonConstructor] - public LoginResponse(string username, string authenticationToken, string refreshToken, bool showRecoveryKey) + public LoginResponse(string username, string authenticationToken, string refreshToken) { Username = username; AuthenticationToken = authenticationToken; RefreshToken = refreshToken; - ShowRecoveryKey = showRecoveryKey; } } diff --git a/Crypter.Common/Contracts/Features/UserConsents/UserConsentRequest.cs b/Crypter.Common/Contracts/Features/UserConsents/UserConsentRequest.cs new file mode 100644 index 000000000..c03f59eeb --- /dev/null +++ b/Crypter.Common/Contracts/Features/UserConsents/UserConsentRequest.cs @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 Crypter File Transfer + * + * This file is part of the Crypter file transfer project. + * + * Crypter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Crypter source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the aforementioned license + * by purchasing a commercial license. Buying such a license is mandatory + * as soon as you develop commercial activities involving the Crypter source + * code without disclosing the source code of your own applications. + * + * Contact the current copyright holder to discuss commercial license options. + */ + +using System.Text.Json.Serialization; + +namespace Crypter.Common.Contracts.Features.UserConsents; + +public sealed record UserConsentRequest +{ + public UserConsentType ConsentType { get; init; } + + [JsonConstructor] + public UserConsentRequest(UserConsentType consentType) + { + ConsentType = consentType; + } +} diff --git a/Crypter.Common/Contracts/Features/UserConsents/UserConsentType.cs b/Crypter.Common/Contracts/Features/UserConsents/UserConsentType.cs new file mode 100644 index 000000000..2b08b7c2e --- /dev/null +++ b/Crypter.Common/Contracts/Features/UserConsents/UserConsentType.cs @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 Crypter File Transfer + * + * This file is part of the Crypter file transfer project. + * + * Crypter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Crypter source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the aforementioned license + * by purchasing a commercial license. Buying such a license is mandatory + * as soon as you develop commercial activities involving the Crypter source + * code without disclosing the source code of your own applications. + * + * Contact the current copyright holder to discuss commercial license options. + */ + +namespace Crypter.Common.Contracts.Features.UserConsents; + +public enum UserConsentType +{ + + TermsOfService, + PrivacyPolicy, + RecoveryKeyRisks +} diff --git a/Crypter.Core/Features/AccountRecovery/Commands/SubmitAccountRecoveryCommand.cs b/Crypter.Core/Features/AccountRecovery/Commands/SubmitAccountRecoveryCommand.cs index be4fb5812..34353e7a0 100644 --- a/Crypter.Core/Features/AccountRecovery/Commands/SubmitAccountRecoveryCommand.cs +++ b/Crypter.Core/Features/AccountRecovery/Commands/SubmitAccountRecoveryCommand.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -29,6 +29,7 @@ using System.Threading; using System.Threading.Tasks; using Crypter.Common.Contracts.Features.AccountRecovery.SubmitRecovery; +using Crypter.Common.Contracts.Features.UserConsents; using Crypter.Common.Infrastructure; using Crypter.Common.Primitives; using Crypter.Core.Features.AccountRecovery.Events; @@ -163,8 +164,8 @@ await result.DoRightAsync(async recoveryResult => UserConsentEntity? latestRecoveryConsent = await _dataContext.UserConsents .Where(x => x.Owner == user.Id) - .Where(x => x.ConsentType == ConsentType.RecoveryKeyRisks) - .OrderBy(x => x.Created) + .Where(x => x.ConsentType == UserConsentType.RecoveryKeyRisks) + .OrderBy(x => x.Activated) .FirstOrDefaultAsync(); if (latestRecoveryConsent is not null) diff --git a/Crypter.Core/Features/UserAuthentication/Commands/UserLoginCommand.cs b/Crypter.Core/Features/UserAuthentication/Commands/UserLoginCommand.cs index 8e3bd6682..4d5dd88ad 100644 --- a/Crypter.Core/Features/UserAuthentication/Commands/UserLoginCommand.cs +++ b/Crypter.Core/Features/UserAuthentication/Commands/UserLoginCommand.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -76,7 +76,7 @@ public UserLoginCommandHandler( _tokenService = tokenService; _clientPasswordVersion = passwordSettings.Value.ClientVersion; - _refreshTokenProviderMap = new Dictionary>() + _refreshTokenProviderMap = new Dictionary> { { TokenType.Session, _tokenService.NewSessionToken }, { TokenType.Device, _tokenService.NewDeviceToken } @@ -143,7 +143,6 @@ private async Task> GetUserAsync(ValidLoginReques _foundUserEntity = await _dataContext.Users .Where(x => x.Username == validLoginRequest.Username.Value) .Include(x => x.FailedLoginAttempts) - .Include(x => x.Consents!.Where(y => y.Active == true)) .Include(x => x.MasterKey) .Include(x => x.KeyPair) .FirstOrDefaultAsync(); @@ -221,11 +220,8 @@ private async Task> CreateLoginResponseAsync(U string authToken = _tokenService.NewAuthenticationToken(userEntity.Id); await Common.PublishRefreshTokenCreatedEventAsync(_publisher, refreshToken); - - bool userHasConsentedToRecoveryKeyRisks = userEntity.Consents! - .Any(x => x.ConsentType == ConsentType.RecoveryKeyRisks); - - return new LoginResponse(userEntity.Username, authToken, refreshToken.Token, !userHasConsentedToRecoveryKeyRisks); + + return new LoginResponse(userEntity.Username, authToken, refreshToken.Token); } private Either> GetValidClientPasswords(List clientPasswords) diff --git a/Crypter.Core/Features/UserConsent/Commands/SaveAcknowledgementOfRecoveryKeyRisksCommand.cs b/Crypter.Core/Features/UserConsent/Commands/SaveAcknowledgementOfRecoveryKeyRisksCommand.cs deleted file mode 100644 index c2dfe3416..000000000 --- a/Crypter.Core/Features/UserConsent/Commands/SaveAcknowledgementOfRecoveryKeyRisksCommand.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Crypter.DataAccess; -using Crypter.DataAccess.Entities; -using EasyMonads; - -namespace Crypter.Core.Features.UserConsent.Commands; - -public record SaveAcknowledgementOfRecoveryKeyRisksCommand(Guid UserId) : MediatR.IRequest; - -// ReSharper disable once ClassNeverInstantiated.Global -internal sealed class SaveAcknowledgementOfRecoveryKeyRisksCommandHandler - : MediatR.IRequestHandler -{ - private readonly DataContext _dataContext; - - public SaveAcknowledgementOfRecoveryKeyRisksCommandHandler(DataContext dataContext) - { - _dataContext = dataContext; - } - - public async Task Handle(SaveAcknowledgementOfRecoveryKeyRisksCommand request, CancellationToken cancellationToken) - { - UserConsentEntity newConsent = - new UserConsentEntity(request.UserId, ConsentType.RecoveryKeyRisks, true, DateTime.UtcNow); - _dataContext.UserConsents.Add(newConsent); - await _dataContext.SaveChangesAsync(CancellationToken.None); - return Unit.Default; - } -} diff --git a/Crypter.Core/Features/UserConsent/Commands/SaveUserConsentCommand.cs b/Crypter.Core/Features/UserConsent/Commands/SaveUserConsentCommand.cs new file mode 100644 index 000000000..22bcacc93 --- /dev/null +++ b/Crypter.Core/Features/UserConsent/Commands/SaveUserConsentCommand.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Crypter.Common.Contracts.Features.UserConsents; +using Crypter.DataAccess; +using Crypter.DataAccess.Entities; +using MediatR; +using Unit = EasyMonads.Unit; + +namespace Crypter.Core.Features.UserConsent.Commands; + +public record SaveUserConsentCommand(Guid UserId, UserConsentType ConsentType) + : IRequest; + +internal sealed class SaveUserConsentCommandHandler + : IRequestHandler +{ + private readonly DataContext _dataContext; + + public SaveUserConsentCommandHandler(DataContext dataContext) + { + _dataContext = dataContext; + } + + public async Task Handle(SaveUserConsentCommand request, CancellationToken _) + { + UserConsentEntity newConsent = new UserConsentEntity(request.UserId, request.ConsentType, true, DateTime.UtcNow); + _dataContext.UserConsents.Add(newConsent); + await _dataContext.SaveChangesAsync(CancellationToken.None); + return Unit.Default; + } +} diff --git a/Crypter.Core/Features/UserConsent/Queries/GetUserConsentsQuery.cs b/Crypter.Core/Features/UserConsent/Queries/GetUserConsentsQuery.cs new file mode 100644 index 000000000..fd4177ee0 --- /dev/null +++ b/Crypter.Core/Features/UserConsent/Queries/GetUserConsentsQuery.cs @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2025 Crypter File Transfer + * + * This file is part of the Crypter file transfer project. + * + * Crypter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Crypter source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the aforementioned license + * by purchasing a commercial license. Buying such a license is mandatory + * as soon as you develop commercial activities involving the Crypter source + * code without disclosing the source code of your own applications. + * + * Contact the current copyright holder to discuss commercial license options. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Crypter.Common.Contracts.Features.UserConsents; +using Crypter.DataAccess; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Crypter.Core.Features.UserConsent.Queries; + +public sealed record GetUserConsentsQuery(Guid UserId) + : IRequest>; + +internal sealed class GetUserConsentsQueryHandler + : IRequestHandler> +{ + private readonly DataContext _dataContext; + + public GetUserConsentsQueryHandler(DataContext dataContext) + { + _dataContext = dataContext; + } + + public async Task> Handle(GetUserConsentsQuery request, CancellationToken cancellationToken) + { + Dictionary activeConsents = await _dataContext.UserConsents + .Where(x => x.Owner == request.UserId && x.Active) + .ToDictionaryAsync(x => x.ConsentType, x => x.Activated, cancellationToken); + + Dictionary returnMap = Enum.GetValues() + .ToDictionary(x => x, _ => (DateTimeOffset?)null); + + foreach (KeyValuePair entry in returnMap) + { + if (activeConsents.TryGetValue(entry.Key, out DateTime consentTime)) + { + returnMap[entry.Key] = new DateTimeOffset(consentTime, TimeSpan.Zero); + } + } + + return returnMap; + } +} diff --git a/Crypter.DataAccess/Entities/UserConsentEntity.cs b/Crypter.DataAccess/Entities/UserConsentEntity.cs index 5eb6ccb44..6507d2d21 100644 --- a/Crypter.DataAccess/Entities/UserConsentEntity.cs +++ b/Crypter.DataAccess/Entities/UserConsentEntity.cs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Crypter File Transfer + * Copyright (C) 2025 Crypter File Transfer * * This file is part of the Crypter file transfer project. * @@ -24,6 +24,7 @@ * Contact the current copyright holder to discuss commercial license options. */ +using Crypter.Common.Contracts.Features.UserConsents; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -43,31 +44,23 @@ public class UserConsentEntity { public long Id { get; set; } public Guid Owner { get; set; } - public ConsentType ConsentType { get; set; } - public DateTime Created { get; set; } + public UserConsentType ConsentType { get; set; } + public DateTime Activated { get; set; } public DateTime? Deactivated { get; set; } public bool Active { get; set; } public UserEntity? User { get; set; } - public UserConsentEntity(Guid owner, ConsentType consentType, bool active, DateTime created, - DateTime? deactivated = null) + public UserConsentEntity(Guid owner, UserConsentType consentType, bool active, DateTime activated, DateTime? deactivated = null) { Owner = owner; ConsentType = consentType; Active = active; - Created = created; + Activated = activated; Deactivated = deactivated; } } -public enum ConsentType -{ - TermsOfService, - PrivacyPolicy, - RecoveryKeyRisks -} - public class UserConsentEntityConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) diff --git a/Crypter.DataAccess/Migrations/20250114045437_RenameUserConsentColumn.Designer.cs b/Crypter.DataAccess/Migrations/20250114045437_RenameUserConsentColumn.Designer.cs new file mode 100644 index 000000000..26960f4ab --- /dev/null +++ b/Crypter.DataAccess/Migrations/20250114045437_RenameUserConsentColumn.Designer.cs @@ -0,0 +1,737 @@ +// +using System; +using System.Text.Json; +using Crypter.DataAccess; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Crypter.DataAccess.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250114045437_RenameUserConsentColumn")] + partial class RenameUserConsentColumn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("crypter") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Crypter.DataAccess.Entities.AnonymousFileTransferEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("KeyExchangeNonce") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Parts") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Proof") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("AnonymousFileTransfer", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.AnonymousMessageTransferEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("KeyExchangeNonce") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Proof") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("AnonymousMessageTransfer", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.EventLogEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdditionalData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("EventLogType") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("EventLogType"); + + b.ToTable("EventLog", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserConsentEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("Activated") + .HasColumnType("timestamp with time zone"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ConsentType") + .HasColumnType("integer"); + + b.Property("Deactivated") + .HasColumnType("timestamp with time zone"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Owner"); + + b.ToTable("UserConsent", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserContactEntity", b => + { + b.Property("OwnerId") + .HasColumnType("uuid") + .HasColumnName("Owner"); + + b.Property("ContactId") + .HasColumnType("uuid") + .HasColumnName("Contact"); + + b.HasKey("OwnerId", "ContactId"); + + b.HasIndex("ContactId"); + + b.ToTable("UserContact", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserEmailVerificationEntity", b => + { + b.Property("Owner") + .HasColumnType("uuid"); + + b.Property("Code") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationKey") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Owner"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("UserEmailVerification", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientPasswordVersion") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((short)0); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("EmailAddress") + .HasColumnType("citext"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("PasswordSalt") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ServerPasswordVersion") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((short)0); + + b.Property("Username") + .IsRequired() + .HasColumnType("citext"); + + b.HasKey("Id"); + + b.HasIndex("EmailAddress") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("User", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserFailedLoginEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Owner"); + + b.ToTable("UserFailedLogin", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserFileTransferEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("KeyExchangeNonce") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Parts") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Proof") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("PublicKey") + .HasColumnType("bytea"); + + b.Property("RecipientId") + .HasColumnType("uuid") + .HasColumnName("Recipient"); + + b.Property("SenderId") + .HasColumnType("uuid") + .HasColumnName("Sender"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RecipientId"); + + b.HasIndex("SenderId"); + + b.ToTable("UserFileTransfer", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserKeyPairEntity", b => + { + b.Property("Owner") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Nonce") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("PrivateKey") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Owner"); + + b.ToTable("UserKeyPair", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserMasterKeyEntity", b => + { + b.Property("Owner") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedKey") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Nonce") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("RecoveryProof") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Owner"); + + b.ToTable("UserMasterKey", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserMessageTransferEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("KeyExchangeNonce") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Proof") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("PublicKey") + .HasColumnType("bytea"); + + b.Property("RecipientId") + .HasColumnType("uuid") + .HasColumnName("Recipient"); + + b.Property("SenderId") + .HasColumnType("uuid") + .HasColumnName("Sender"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RecipientId"); + + b.HasIndex("SenderId"); + + b.ToTable("UserMessageTransfer", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserNotificationSettingEntity", b => + { + b.Property("Owner") + .HasColumnType("uuid"); + + b.Property("EmailNotifications") + .HasColumnType("boolean"); + + b.Property("EnableTransferNotifications") + .HasColumnType("boolean"); + + b.HasKey("Owner"); + + b.ToTable("UserNotificationSetting", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserPrivacySettingEntity", b => + { + b.Property("Owner") + .HasColumnType("uuid"); + + b.Property("AllowKeyExchangeRequests") + .HasColumnType("boolean"); + + b.Property("ReceiveFiles") + .HasColumnType("integer"); + + b.Property("ReceiveMessages") + .HasColumnType("integer"); + + b.Property("Visibility") + .HasColumnType("integer"); + + b.HasKey("Owner"); + + b.ToTable("UserPrivacySetting", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserProfileEntity", b => + { + b.Property("Owner") + .HasColumnType("uuid"); + + b.Property("About") + .IsRequired() + .HasColumnType("text"); + + b.Property("Alias") + .IsRequired() + .HasColumnType("text"); + + b.Property("Image") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Owner"); + + b.ToTable("UserProfile", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserRecoveryEntity", b => + { + b.Property("Owner") + .HasColumnType("uuid"); + + b.Property("Code") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationKey") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Owner"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("UserRecovery", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Owner"); + + b.ToTable("UserToken", "crypter"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserConsentEntity", b => + { + b.HasOne("Crypter.DataAccess.Entities.UserEntity", "User") + .WithMany("Consents") + .HasForeignKey("Owner") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserContactEntity", b => + { + b.HasOne("Crypter.DataAccess.Entities.UserEntity", "Contact") + .WithMany("Contactors") + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Crypter.DataAccess.Entities.UserEntity", "Owner") + .WithMany("Contacts") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserEmailVerificationEntity", b => + { + b.HasOne("Crypter.DataAccess.Entities.UserEntity", "User") + .WithOne("EmailVerification") + .HasForeignKey("Crypter.DataAccess.Entities.UserEmailVerificationEntity", "Owner") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserFailedLoginEntity", b => + { + b.HasOne("Crypter.DataAccess.Entities.UserEntity", "User") + .WithMany("FailedLoginAttempts") + .HasForeignKey("Owner") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserFileTransferEntity", b => + { + b.HasOne("Crypter.DataAccess.Entities.UserEntity", "Recipient") + .WithMany("ReceivedFileTransfers") + .HasForeignKey("RecipientId"); + + b.HasOne("Crypter.DataAccess.Entities.UserEntity", "Sender") + .WithMany("SentFileTransfers") + .HasForeignKey("SenderId"); + + b.Navigation("Recipient"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserKeyPairEntity", b => + { + b.HasOne("Crypter.DataAccess.Entities.UserEntity", "User") + .WithOne("KeyPair") + .HasForeignKey("Crypter.DataAccess.Entities.UserKeyPairEntity", "Owner") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserMasterKeyEntity", b => + { + b.HasOne("Crypter.DataAccess.Entities.UserEntity", "User") + .WithOne("MasterKey") + .HasForeignKey("Crypter.DataAccess.Entities.UserMasterKeyEntity", "Owner") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserMessageTransferEntity", b => + { + b.HasOne("Crypter.DataAccess.Entities.UserEntity", "Recipient") + .WithMany("ReceivedMessageTransfers") + .HasForeignKey("RecipientId"); + + b.HasOne("Crypter.DataAccess.Entities.UserEntity", "Sender") + .WithMany("SentMessageTransfers") + .HasForeignKey("SenderId"); + + b.Navigation("Recipient"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserNotificationSettingEntity", b => + { + b.HasOne("Crypter.DataAccess.Entities.UserEntity", "User") + .WithOne("NotificationSetting") + .HasForeignKey("Crypter.DataAccess.Entities.UserNotificationSettingEntity", "Owner") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserPrivacySettingEntity", b => + { + b.HasOne("Crypter.DataAccess.Entities.UserEntity", "User") + .WithOne("PrivacySetting") + .HasForeignKey("Crypter.DataAccess.Entities.UserPrivacySettingEntity", "Owner") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserProfileEntity", b => + { + b.HasOne("Crypter.DataAccess.Entities.UserEntity", "User") + .WithOne("Profile") + .HasForeignKey("Crypter.DataAccess.Entities.UserProfileEntity", "Owner") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserRecoveryEntity", b => + { + b.HasOne("Crypter.DataAccess.Entities.UserEntity", "User") + .WithOne("Recovery") + .HasForeignKey("Crypter.DataAccess.Entities.UserRecoveryEntity", "Owner") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserTokenEntity", b => + { + b.HasOne("Crypter.DataAccess.Entities.UserEntity", "User") + .WithMany("Tokens") + .HasForeignKey("Owner") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crypter.DataAccess.Entities.UserEntity", b => + { + b.Navigation("Consents"); + + b.Navigation("Contactors"); + + b.Navigation("Contacts"); + + b.Navigation("EmailVerification"); + + b.Navigation("FailedLoginAttempts"); + + b.Navigation("KeyPair"); + + b.Navigation("MasterKey"); + + b.Navigation("NotificationSetting"); + + b.Navigation("PrivacySetting"); + + b.Navigation("Profile"); + + b.Navigation("ReceivedFileTransfers"); + + b.Navigation("ReceivedMessageTransfers"); + + b.Navigation("Recovery"); + + b.Navigation("SentFileTransfers"); + + b.Navigation("SentMessageTransfers"); + + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Crypter.DataAccess/Migrations/20250114045437_RenameUserConsentColumn.cs b/Crypter.DataAccess/Migrations/20250114045437_RenameUserConsentColumn.cs new file mode 100644 index 000000000..9c414a72f --- /dev/null +++ b/Crypter.DataAccess/Migrations/20250114045437_RenameUserConsentColumn.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Crypter.DataAccess.Migrations +{ + /// + public partial class RenameUserConsentColumn : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Created", + schema: "crypter", + table: "UserConsent", + newName: "Activated"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Activated", + schema: "crypter", + table: "UserConsent", + newName: "Created"); + } + } +} diff --git a/Crypter.DataAccess/Migrations/DataContextModelSnapshot.cs b/Crypter.DataAccess/Migrations/DataContextModelSnapshot.cs index 5987a3fad..384b34a7f 100644 --- a/Crypter.DataAccess/Migrations/DataContextModelSnapshot.cs +++ b/Crypter.DataAccess/Migrations/DataContextModelSnapshot.cs @@ -19,7 +19,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("crypter") - .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext"); @@ -137,15 +137,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + b.Property("Activated") + .HasColumnType("timestamp with time zone"); + b.Property("Active") .HasColumnType("boolean"); b.Property("ConsentType") .HasColumnType("integer"); - b.Property("Created") - .HasColumnType("timestamp with time zone"); - b.Property("Deactivated") .HasColumnType("timestamp with time zone"); diff --git a/Crypter.Test/Integration_Tests/UserConsent_Tests/RecoveryKeyRisksConsent_Tests.cs b/Crypter.Test/Integration_Tests/UserConsent_Tests/RecoveryKeyRisksConsent_Tests.cs index d41d20622..5a97c2b8a 100644 --- a/Crypter.Test/Integration_Tests/UserConsent_Tests/RecoveryKeyRisksConsent_Tests.cs +++ b/Crypter.Test/Integration_Tests/UserConsent_Tests/RecoveryKeyRisksConsent_Tests.cs @@ -28,6 +28,7 @@ using Crypter.Common.Client.Interfaces.HttpClients; using Crypter.Common.Client.Interfaces.Repositories; using Crypter.Common.Contracts.Features.UserAuthentication; +using Crypter.Common.Contracts.Features.UserConsents; using Crypter.Common.Enums; using EasyMonads; using Microsoft.AspNetCore.Mvc.Testing; @@ -63,8 +64,7 @@ public async Task TeardownTestAsync() [Test] public async Task Consent_To_Recovery_Key_Risks_Works_Async() { - RegistrationRequest registrationRequest = - TestData.GetRegistrationRequest(TestData.DefaultUsername, TestData.DefaultPassword); + RegistrationRequest registrationRequest = TestData.GetRegistrationRequest(TestData.DefaultUsername, TestData.DefaultPassword); Either registrationResult = await _client!.UserAuthentication.RegisterAsync(registrationRequest); LoginRequest loginRequest = TestData.GetLoginRequest(TestData.DefaultUsername, TestData.DefaultPassword); @@ -76,7 +76,7 @@ await loginResult.DoRightAsync(async loginResponse => await _clientTokenRepository!.StoreRefreshTokenAsync(loginResponse.RefreshToken, TokenType.Session); }); - Maybe result = await _client!.UserConsent.ConsentToRecoveryKeyRisksAsync(); + Maybe result = await _client!.UserConsent.ConsentAsync(UserConsentType.RecoveryKeyRisks); Assert.That(registrationResult.IsRight, Is.True); Assert.That(loginResult.IsRight, Is.True); diff --git a/Crypter.Web/Shared/Modal/RecoveryKeyModal.razor.cs b/Crypter.Web/Shared/Modal/RecoveryKeyModal.razor.cs index f60e1e1c7..02547e4fc 100644 --- a/Crypter.Web/Shared/Modal/RecoveryKeyModal.razor.cs +++ b/Crypter.Web/Shared/Modal/RecoveryKeyModal.razor.cs @@ -28,6 +28,7 @@ using Crypter.Common.Client.Interfaces.HttpClients; using Crypter.Common.Client.Interfaces.Services; using Crypter.Common.Client.Models; +using Crypter.Common.Contracts.Features.UserConsents; using Crypter.Web.Shared.Modal.Template; using EasyMonads; using Microsoft.AspNetCore.Components; @@ -65,7 +66,7 @@ private async Task CopyRecoveryKeyToClipboardAsync() private async Task OnAcknowledgedClickedAsync() { - await CrypterApiService.UserConsent.ConsentToRecoveryKeyRisksAsync(); + await CrypterApiService.UserConsent.ConsentAsync(UserConsentType.RecoveryKeyRisks); _modalBehaviorRef.Close(); } } From 3eb356ff9c0c8faebc41328d71372c4178431b40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Feb 2025 12:55:18 +0000 Subject: [PATCH 14/23] Bump Azure.Identity from 1.13.1 to 1.13.2 Bumps [Azure.Identity](https://github.com/Azure/azure-sdk-for-net) from 1.13.1 to 1.13.2. - [Release notes](https://github.com/Azure/azure-sdk-for-net/releases) - [Commits](https://github.com/Azure/azure-sdk-for-net/compare/Azure.Identity_1.13.1...Azure.Identity_1.13.2) --- updated-dependencies: - dependency-name: Azure.Identity dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Crypter.Test/Crypter.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Crypter.Test/Crypter.Test.csproj b/Crypter.Test/Crypter.Test.csproj index 5fb566404..0f7de312a 100644 --- a/Crypter.Test/Crypter.Test.csproj +++ b/Crypter.Test/Crypter.Test.csproj @@ -13,7 +13,7 @@ - + From e2f2258af2dea2f9880178201628a2b8004d5086 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Feb 2025 12:56:13 +0000 Subject: [PATCH 15/23] Bump MailKit from 4.9.0 to 4.10.0 Bumps [MailKit](https://github.com/jstedfast/MailKit) from 4.9.0 to 4.10.0. - [Changelog](https://github.com/jstedfast/MailKit/blob/master/ReleaseNotes.md) - [Commits](https://github.com/jstedfast/MailKit/compare/4.9.0...4.10.0) --- updated-dependencies: - dependency-name: MailKit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Crypter.Core/Crypter.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Crypter.Core/Crypter.Core.csproj b/Crypter.Core/Crypter.Core.csproj index 26617c98b..8b4942a0f 100644 --- a/Crypter.Core/Crypter.Core.csproj +++ b/Crypter.Core/Crypter.Core.csproj @@ -12,7 +12,7 @@ - + From eac49c9faf27464ce50683134511910b4680de14 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Feb 2025 12:56:30 +0000 Subject: [PATCH 16/23] Bump Npgsql and Npgsql.EntityFrameworkCore.PostgreSQL Bumps [Npgsql](https://github.com/npgsql/npgsql) and [Npgsql.EntityFrameworkCore.PostgreSQL](https://github.com/npgsql/efcore.pg). These dependencies needed to be updated together. Updates `Npgsql` from 9.0.2 to 9.0.2 - [Release notes](https://github.com/npgsql/npgsql/releases) - [Commits](https://github.com/npgsql/npgsql/compare/v9.0.2...v9.0.2) Updates `Npgsql.EntityFrameworkCore.PostgreSQL` from 9.0.2 to 9.0.3 - [Release notes](https://github.com/npgsql/efcore.pg/releases) - [Commits](https://github.com/npgsql/efcore.pg/compare/v9.0.2...v9.0.3) --- updated-dependencies: - dependency-name: Npgsql dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: Npgsql.EntityFrameworkCore.PostgreSQL dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Crypter.DataAccess/Crypter.DataAccess.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Crypter.DataAccess/Crypter.DataAccess.csproj b/Crypter.DataAccess/Crypter.DataAccess.csproj index afb05bdd2..b73a00744 100644 --- a/Crypter.DataAccess/Crypter.DataAccess.csproj +++ b/Crypter.DataAccess/Crypter.DataAccess.csproj @@ -9,7 +9,7 @@ - + From 70b658516db8d0b291c1e3bd3cb830119ce8c368 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Feb 2025 12:56:49 +0000 Subject: [PATCH 17/23] Bump Microsoft.AspNetCore.Components.WebAssembly.DevServer Bumps [Microsoft.AspNetCore.Components.WebAssembly.DevServer](https://github.com/dotnet/aspnetcore) from 9.0.0 to 9.0.1. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md) - [Commits](https://github.com/dotnet/aspnetcore/compare/v9.0.0...v9.0.1) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.Components.WebAssembly.DevServer dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Crypter.Web/Crypter.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Crypter.Web/Crypter.Web.csproj b/Crypter.Web/Crypter.Web.csproj index 7ba41c3a5..8e8d7fc2a 100644 --- a/Crypter.Web/Crypter.Web.csproj +++ b/Crypter.Web/Crypter.Web.csproj @@ -28,7 +28,7 @@ - + From 01f2e92b22b822d99faedb06ef4a93bbd92a4adc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 9 Feb 2025 04:45:21 +0000 Subject: [PATCH 18/23] Bump Microsoft.AspNetCore.Mvc.Testing from 9.0.0 to 9.0.1 Bumps [Microsoft.AspNetCore.Mvc.Testing](https://github.com/dotnet/aspnetcore) from 9.0.0 to 9.0.1. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md) - [Commits](https://github.com/dotnet/aspnetcore/compare/v9.0.0...v9.0.1) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.Mvc.Testing dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Crypter.Test.Web/Crypter.Test.Web.csproj | 2 +- Crypter.Test/Crypter.Test.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Crypter.Test.Web/Crypter.Test.Web.csproj b/Crypter.Test.Web/Crypter.Test.Web.csproj index 6bb474c97..fb1c7fdd6 100644 --- a/Crypter.Test.Web/Crypter.Test.Web.csproj +++ b/Crypter.Test.Web/Crypter.Test.Web.csproj @@ -8,7 +8,7 @@ - + diff --git a/Crypter.Test/Crypter.Test.csproj b/Crypter.Test/Crypter.Test.csproj index 0f7de312a..e7441cfb8 100644 --- a/Crypter.Test/Crypter.Test.csproj +++ b/Crypter.Test/Crypter.Test.csproj @@ -14,7 +14,7 @@ - + From fae645f95774c84e0486bd1b746ba0d8654df210 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sat, 8 Feb 2025 22:52:04 -0600 Subject: [PATCH 19/23] Remove [FromForm] from IFormFile to fix SwaggerGen --- Crypter.API/Controllers/FileTransferController.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Crypter.API/Controllers/FileTransferController.cs b/Crypter.API/Controllers/FileTransferController.cs index 94f268557..fa9f31d58 100644 --- a/Crypter.API/Controllers/FileTransferController.cs +++ b/Crypter.API/Controllers/FileTransferController.cs @@ -112,8 +112,7 @@ public async Task InitializeMultipartFileTransferAsync([FromQuery [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UploadTransferResponse))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))] - public async Task UploadMultipartFileTransferAsync([FromQuery] string id, [FromQuery] int position, - [FromForm] IFormFile? ciphertext) + public async Task UploadMultipartFileTransferAsync([FromQuery] string id, [FromQuery] int position, IFormFile? ciphertext) { SaveMultipartFileTransferCommand command = new SaveMultipartFileTransferCommand(UserId, id, position, ciphertext?.OpenReadStream()); return await _sender.Send(command) From b96175a0754c0ff93688794ec920437e392b8c4e Mon Sep 17 00:00:00 2001 From: Istvan Bede Date: Wed, 12 Feb 2025 09:41:09 +0100 Subject: [PATCH 20/23] Use AsNoTracking and switch to existence test where applicable --- .../Features/Keys/Commands/InsertKeyPairCommand.cs | 10 ++++------ .../Features/Keys/Commands/UpsertMasterKeyCommand.cs | 6 +++--- .../Reports/Queries/ApplicationAnalyticsReportQuery.cs | 2 +- Crypter.Core/Features/UserAuthentication/Common.cs | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Crypter.Core/Features/Keys/Commands/InsertKeyPairCommand.cs b/Crypter.Core/Features/Keys/Commands/InsertKeyPairCommand.cs index c720b920a..4ce340959 100644 --- a/Crypter.Core/Features/Keys/Commands/InsertKeyPairCommand.cs +++ b/Crypter.Core/Features/Keys/Commands/InsertKeyPairCommand.cs @@ -52,19 +52,17 @@ public InsertKeyPairCommandHandler(DataContext dataContext) public async Task> Handle(InsertKeyPairCommand request, CancellationToken cancellationToken) { - UserKeyPairEntity? keyPairEntity = await _dataContext.UserKeyPairs - .FirstOrDefaultAsync(x => x.Owner == request.UserId, CancellationToken.None); + bool keyPairExists = await _dataContext.UserKeyPairs.AnyAsync(x => x.Owner == request.UserId, CancellationToken.None); - if (keyPairEntity is null) + if (!keyPairExists) { UserKeyPairEntity newEntity = new UserKeyPairEntity(request.UserId, request.Data.EncryptedPrivateKey, request.Data.PublicKey, request.Data.Nonce, DateTime.UtcNow); _dataContext.UserKeyPairs.Add(newEntity); await _dataContext.SaveChangesAsync(CancellationToken.None); + return Unit.Default; } - return keyPairEntity is null - ? Unit.Default - : InsertKeyPairError.KeyPairAlreadyExists; + return InsertKeyPairError.KeyPairAlreadyExists; } } diff --git a/Crypter.Core/Features/Keys/Commands/UpsertMasterKeyCommand.cs b/Crypter.Core/Features/Keys/Commands/UpsertMasterKeyCommand.cs index 8b28fd552..8800b25ee 100644 --- a/Crypter.Core/Features/Keys/Commands/UpsertMasterKeyCommand.cs +++ b/Crypter.Core/Features/Keys/Commands/UpsertMasterKeyCommand.cs @@ -80,10 +80,10 @@ public async Task> Handle(UpsertMasterKeyComm _ => InsertMasterKeyError.InvalidPassword, async _ => { - UserMasterKeyEntity? masterKeyEntity = await _dataContext.UserMasterKeys - .FirstOrDefaultAsync(x => x.Owner == request.UserId, CancellationToken.None); + bool masterKeyExists = await _dataContext.UserMasterKeys + .AnyAsync(x => x.Owner == request.UserId, CancellationToken.None); - if (masterKeyEntity is not null && !request.AllowReplacement) + if (masterKeyExists && !request.AllowReplacement) { return InsertMasterKeyError.Conflict; } diff --git a/Crypter.Core/Features/Reports/Queries/ApplicationAnalyticsReportQuery.cs b/Crypter.Core/Features/Reports/Queries/ApplicationAnalyticsReportQuery.cs index 9467aef9e..845507cf2 100644 --- a/Crypter.Core/Features/Reports/Queries/ApplicationAnalyticsReportQuery.cs +++ b/Crypter.Core/Features/Reports/Queries/ApplicationAnalyticsReportQuery.cs @@ -57,7 +57,7 @@ public async Task Handle(ApplicationAnalyticsReportQ DateTimeOffset now = DateTimeOffset.UtcNow; DateTimeOffset reportBegin = DateTimeOffset.UtcNow.AddDays(-request.ReportPeriodDays); - List events = await _dataContext.EventLogs + List events = await _dataContext.EventLogs.AsNoTracking() .Where(eventLog => eventLog.Timestamp <= now && eventLog.Timestamp >= reportBegin) .ToListAsync(cancellationToken); diff --git a/Crypter.Core/Features/UserAuthentication/Common.cs b/Crypter.Core/Features/UserAuthentication/Common.cs index b5f145209..713a22f1b 100644 --- a/Crypter.Core/Features/UserAuthentication/Common.cs +++ b/Crypter.Core/Features/UserAuthentication/Common.cs @@ -92,7 +92,7 @@ Maybe VerifyUserPasswordIsMigrated(UserEntity user, T error) Task> FetchUserAsync(T error) { return Either.FromRightAsync( - dataContext.Users.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken), + dataContext.Users.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId, cancellationToken), error); } From f7e27d033c06fb63ad3c036bdcdce7241fef9b1a Mon Sep 17 00:00:00 2001 From: Istvan Bede Date: Wed, 12 Feb 2025 09:41:54 +0100 Subject: [PATCH 21/23] Remove unused data contexts --- .../Events/FailedMultipartFileTransferAbandonEvent.cs | 4 ---- .../Events/SuccessfulMultipartFileTransferAbandonEvent.cs | 4 ---- 2 files changed, 8 deletions(-) diff --git a/Crypter.Core/Features/Transfer/Events/FailedMultipartFileTransferAbandonEvent.cs b/Crypter.Core/Features/Transfer/Events/FailedMultipartFileTransferAbandonEvent.cs index e3e252e08..41b01880e 100644 --- a/Crypter.Core/Features/Transfer/Events/FailedMultipartFileTransferAbandonEvent.cs +++ b/Crypter.Core/Features/Transfer/Events/FailedMultipartFileTransferAbandonEvent.cs @@ -30,7 +30,6 @@ using Crypter.Common.Contracts.Features.Transfer; using Crypter.Common.Enums; using Crypter.Core.Services; -using Crypter.DataAccess; using Hangfire; using MediatR; @@ -43,16 +42,13 @@ internal class FailedMultipartFileTransferAbandonEventHandler : INotificationHandler { private readonly IBackgroundJobClient _backgroundJobClient; - private readonly DataContext _dataContext; private readonly IHangfireBackgroundService _hangfireBackgroundService; public FailedMultipartFileTransferAbandonEventHandler( IBackgroundJobClient backgroundJobClient, - DataContext dataContext, IHangfireBackgroundService hangfireBackgroundService) { _backgroundJobClient = backgroundJobClient; - _dataContext = dataContext; _hangfireBackgroundService = hangfireBackgroundService; } diff --git a/Crypter.Core/Features/Transfer/Events/SuccessfulMultipartFileTransferAbandonEvent.cs b/Crypter.Core/Features/Transfer/Events/SuccessfulMultipartFileTransferAbandonEvent.cs index ec8f343ab..4574efca9 100644 --- a/Crypter.Core/Features/Transfer/Events/SuccessfulMultipartFileTransferAbandonEvent.cs +++ b/Crypter.Core/Features/Transfer/Events/SuccessfulMultipartFileTransferAbandonEvent.cs @@ -29,7 +29,6 @@ using System.Threading.Tasks; using Crypter.Common.Enums; using Crypter.Core.Services; -using Crypter.DataAccess; using Hangfire; using MediatR; @@ -42,16 +41,13 @@ internal class SuccessfulMultipartFileTransferAbandonEventHandler : INotificationHandler { private readonly IBackgroundJobClient _backgroundJobClient; - private readonly DataContext _dataContext; private readonly IHangfireBackgroundService _hangfireBackgroundService; public SuccessfulMultipartFileTransferAbandonEventHandler( IBackgroundJobClient backgroundJobClient, - DataContext dataContext, IHangfireBackgroundService hangfireBackgroundService) { _backgroundJobClient = backgroundJobClient; - _dataContext = dataContext; _hangfireBackgroundService = hangfireBackgroundService; } From cf8d2d4dfb1b254fb9460299f10c250900994dab Mon Sep 17 00:00:00 2001 From: Istvan Bede Date: Wed, 12 Feb 2025 09:41:09 +0100 Subject: [PATCH 22/23] Use AsNoTracking and switch to existence test where applicable --- .../Features/Keys/Commands/InsertKeyPairCommand.cs | 10 ++++------ .../Features/Keys/Commands/UpsertMasterKeyCommand.cs | 6 +++--- .../Reports/Queries/ApplicationAnalyticsReportQuery.cs | 2 +- Crypter.Core/Features/UserAuthentication/Common.cs | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Crypter.Core/Features/Keys/Commands/InsertKeyPairCommand.cs b/Crypter.Core/Features/Keys/Commands/InsertKeyPairCommand.cs index c720b920a..4ce340959 100644 --- a/Crypter.Core/Features/Keys/Commands/InsertKeyPairCommand.cs +++ b/Crypter.Core/Features/Keys/Commands/InsertKeyPairCommand.cs @@ -52,19 +52,17 @@ public InsertKeyPairCommandHandler(DataContext dataContext) public async Task> Handle(InsertKeyPairCommand request, CancellationToken cancellationToken) { - UserKeyPairEntity? keyPairEntity = await _dataContext.UserKeyPairs - .FirstOrDefaultAsync(x => x.Owner == request.UserId, CancellationToken.None); + bool keyPairExists = await _dataContext.UserKeyPairs.AnyAsync(x => x.Owner == request.UserId, CancellationToken.None); - if (keyPairEntity is null) + if (!keyPairExists) { UserKeyPairEntity newEntity = new UserKeyPairEntity(request.UserId, request.Data.EncryptedPrivateKey, request.Data.PublicKey, request.Data.Nonce, DateTime.UtcNow); _dataContext.UserKeyPairs.Add(newEntity); await _dataContext.SaveChangesAsync(CancellationToken.None); + return Unit.Default; } - return keyPairEntity is null - ? Unit.Default - : InsertKeyPairError.KeyPairAlreadyExists; + return InsertKeyPairError.KeyPairAlreadyExists; } } diff --git a/Crypter.Core/Features/Keys/Commands/UpsertMasterKeyCommand.cs b/Crypter.Core/Features/Keys/Commands/UpsertMasterKeyCommand.cs index 8b28fd552..8800b25ee 100644 --- a/Crypter.Core/Features/Keys/Commands/UpsertMasterKeyCommand.cs +++ b/Crypter.Core/Features/Keys/Commands/UpsertMasterKeyCommand.cs @@ -80,10 +80,10 @@ public async Task> Handle(UpsertMasterKeyComm _ => InsertMasterKeyError.InvalidPassword, async _ => { - UserMasterKeyEntity? masterKeyEntity = await _dataContext.UserMasterKeys - .FirstOrDefaultAsync(x => x.Owner == request.UserId, CancellationToken.None); + bool masterKeyExists = await _dataContext.UserMasterKeys + .AnyAsync(x => x.Owner == request.UserId, CancellationToken.None); - if (masterKeyEntity is not null && !request.AllowReplacement) + if (masterKeyExists && !request.AllowReplacement) { return InsertMasterKeyError.Conflict; } diff --git a/Crypter.Core/Features/Reports/Queries/ApplicationAnalyticsReportQuery.cs b/Crypter.Core/Features/Reports/Queries/ApplicationAnalyticsReportQuery.cs index 9467aef9e..845507cf2 100644 --- a/Crypter.Core/Features/Reports/Queries/ApplicationAnalyticsReportQuery.cs +++ b/Crypter.Core/Features/Reports/Queries/ApplicationAnalyticsReportQuery.cs @@ -57,7 +57,7 @@ public async Task Handle(ApplicationAnalyticsReportQ DateTimeOffset now = DateTimeOffset.UtcNow; DateTimeOffset reportBegin = DateTimeOffset.UtcNow.AddDays(-request.ReportPeriodDays); - List events = await _dataContext.EventLogs + List events = await _dataContext.EventLogs.AsNoTracking() .Where(eventLog => eventLog.Timestamp <= now && eventLog.Timestamp >= reportBegin) .ToListAsync(cancellationToken); diff --git a/Crypter.Core/Features/UserAuthentication/Common.cs b/Crypter.Core/Features/UserAuthentication/Common.cs index b5f145209..713a22f1b 100644 --- a/Crypter.Core/Features/UserAuthentication/Common.cs +++ b/Crypter.Core/Features/UserAuthentication/Common.cs @@ -92,7 +92,7 @@ Maybe VerifyUserPasswordIsMigrated(UserEntity user, T error) Task> FetchUserAsync(T error) { return Either.FromRightAsync( - dataContext.Users.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken), + dataContext.Users.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId, cancellationToken), error); } From a514b7d101c2906bbb63b6fec1e9f00b120f8cc7 Mon Sep 17 00:00:00 2001 From: Istvan Bede Date: Wed, 12 Feb 2025 09:41:54 +0100 Subject: [PATCH 23/23] Remove unused data contexts --- .../Events/FailedMultipartFileTransferAbandonEvent.cs | 4 ---- .../Events/SuccessfulMultipartFileTransferAbandonEvent.cs | 4 ---- 2 files changed, 8 deletions(-) diff --git a/Crypter.Core/Features/Transfer/Events/FailedMultipartFileTransferAbandonEvent.cs b/Crypter.Core/Features/Transfer/Events/FailedMultipartFileTransferAbandonEvent.cs index e3e252e08..41b01880e 100644 --- a/Crypter.Core/Features/Transfer/Events/FailedMultipartFileTransferAbandonEvent.cs +++ b/Crypter.Core/Features/Transfer/Events/FailedMultipartFileTransferAbandonEvent.cs @@ -30,7 +30,6 @@ using Crypter.Common.Contracts.Features.Transfer; using Crypter.Common.Enums; using Crypter.Core.Services; -using Crypter.DataAccess; using Hangfire; using MediatR; @@ -43,16 +42,13 @@ internal class FailedMultipartFileTransferAbandonEventHandler : INotificationHandler { private readonly IBackgroundJobClient _backgroundJobClient; - private readonly DataContext _dataContext; private readonly IHangfireBackgroundService _hangfireBackgroundService; public FailedMultipartFileTransferAbandonEventHandler( IBackgroundJobClient backgroundJobClient, - DataContext dataContext, IHangfireBackgroundService hangfireBackgroundService) { _backgroundJobClient = backgroundJobClient; - _dataContext = dataContext; _hangfireBackgroundService = hangfireBackgroundService; } diff --git a/Crypter.Core/Features/Transfer/Events/SuccessfulMultipartFileTransferAbandonEvent.cs b/Crypter.Core/Features/Transfer/Events/SuccessfulMultipartFileTransferAbandonEvent.cs index ec8f343ab..4574efca9 100644 --- a/Crypter.Core/Features/Transfer/Events/SuccessfulMultipartFileTransferAbandonEvent.cs +++ b/Crypter.Core/Features/Transfer/Events/SuccessfulMultipartFileTransferAbandonEvent.cs @@ -29,7 +29,6 @@ using System.Threading.Tasks; using Crypter.Common.Enums; using Crypter.Core.Services; -using Crypter.DataAccess; using Hangfire; using MediatR; @@ -42,16 +41,13 @@ internal class SuccessfulMultipartFileTransferAbandonEventHandler : INotificationHandler { private readonly IBackgroundJobClient _backgroundJobClient; - private readonly DataContext _dataContext; private readonly IHangfireBackgroundService _hangfireBackgroundService; public SuccessfulMultipartFileTransferAbandonEventHandler( IBackgroundJobClient backgroundJobClient, - DataContext dataContext, IHangfireBackgroundService hangfireBackgroundService) { _backgroundJobClient = backgroundJobClient; - _dataContext = dataContext; _hangfireBackgroundService = hangfireBackgroundService; }