Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Cryptie.Client/Core/Navigation/ContentCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,25 @@ namespace Cryptie.Client.Core.Navigation;
public class ContentCoordinator(DashboardViewModel dashboard, IViewModelFactory factory, IScreen screen)
: IContentCoordinator
{
/// <summary>
/// Displays the chat list within the dashboard content region.
/// </summary>
public void ShowChats()
{
dashboard.Content = factory.Create<ChatsViewModel>(screen);
}

/// <summary>
/// Displays the current user's account view.
/// </summary>
public void ShowAccount()
{
dashboard.Content = factory.Create<AccountViewModel>(screen);
}

/// <summary>
/// Displays the application settings view.
/// </summary>
public void ShowSettings()
{
dashboard.Content = factory.Create<SettingsViewModel>(screen);
Expand Down
48 changes: 48 additions & 0 deletions src/Cryptie.Client/Core/Navigation/ShellCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ ShellStateDependencies stateDeps
{
public RoutingState Router { get; } = new();

/// <summary>
/// Initializes the application routing based on the persisted session.
/// </summary>
public async Task StartAsync()
{
if (!TryInitializeSession(out var sessionToken))
Expand Down Expand Up @@ -63,11 +66,17 @@ public async Task StartAsync()
}
}

/// <summary>
/// Navigates to the login view.
/// </summary>
public void ShowLogin()
{
NavigateTo<LoginViewModel>();
}

/// <summary>
/// Clears the navigation stack and shows the login screen.
/// </summary>
public void ResetAndShowLogin()
{
var vm = factory.Create<LoginViewModel>(this);
Expand All @@ -78,31 +87,51 @@ public void ResetAndShowLogin()
.Subscribe();
}

/// <summary>
/// Navigates to the registration view.
/// </summary>
public void ShowRegister()
{
NavigateTo<RegisterViewModel>();
}

/// <summary>
/// Navigates to the TOTP QR setup view.
/// </summary>
public void ShowQrSetup()
{
NavigateTo<TotpQrSetupViewModel>();
}

/// <summary>
/// Navigates to the TOTP code view.
/// </summary>
public void ShowTotpCode()
{
NavigateTo<TotpCodeViewModel>();
}

/// <summary>
/// Navigates to the dashboard view.
/// </summary>
public void ShowDashboard()
{
NavigateTo<DashboardViewModel>();
}

/// <summary>
/// Navigates to the pin code setup view.
/// </summary>
public void ShowPinSetup()
{
NavigateTo<PinCodeViewModel>();
}

/// <summary>
/// Attempts to read the persisted session token from the keychain.
/// </summary>
/// <param name="sessionToken">Outputs the parsed session token when successful.</param>
/// <returns><c>true</c> if a valid token was retrieved.</returns>
private bool TryInitializeSession(out Guid sessionToken)
{
sessionToken = Guid.Empty;
Expand All @@ -117,13 +146,23 @@ private bool TryInitializeSession(out Guid sessionToken)
return true;
}

/// <summary>
/// Gets the user's GUID associated with the provided session token.
/// </summary>
/// <param name="sessionToken">Valid session token.</param>
/// <returns>User GUID or <see cref="Guid.Empty"/> when not found.</returns>
private async Task<Guid> GetUserGuidAsync(Guid sessionToken)
{
var dto = new UserGuidFromTokenRequestDto { SessionToken = sessionToken };
var result = await userDetailsService.GetUserGuidFromTokenAsync(dto);
return result?.Guid ?? Guid.Empty;
}

/// <summary>
/// Loads the current user's private key from the keychain and populates user state.
/// </summary>
/// <param name="userGuid">Identifier of the authenticated user.</param>
/// <returns><c>true</c> when the key was successfully loaded.</returns>
private bool TryInitializeUser(Guid userGuid)
{
stateDeps.UserState.UserId = userGuid;
Expand All @@ -137,6 +176,9 @@ private bool TryInitializeUser(Guid userGuid)
return true;
}

/// <summary>
/// Clears any cached authentication information from state and keychain.
/// </summary>
private void ClearUserState()
{
keychain.TryClearSessionToken(out _);
Expand All @@ -156,13 +198,19 @@ private void ClearUserState()
stateDeps.RegistrationState.LastResponse = null;
}

/// <summary>
/// Determines whether the HTTP exception represents an authentication failure.
/// </summary>
private static bool IsAuthError(HttpRequestException ex)
{
return ex.StatusCode is HttpStatusCode.Unauthorized
or HttpStatusCode.Forbidden
or HttpStatusCode.BadRequest;
}

/// <summary>
/// Helper method to create and navigate to a view model instance.
/// </summary>
private void NavigateTo<TViewModel>() where TViewModel : RoutableViewModelBase
{
var vm = factory.Create<TViewModel>(this);
Expand Down
9 changes: 9 additions & 0 deletions src/Cryptie.Client/Core/Services/ConnectionMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,19 @@ private readonly IServerStatus

public IObservable<bool> ConnectionStatusChanged => _subject;

/// <summary>
/// Performs a single check against the backend service.
/// </summary>
/// <returns><c>true</c> when the backend responds successfully.</returns>
public async Task<bool> IsBackendAliveAsync()
{
return await CheckServerAsync(CancellationToken.None);
}

/// <summary>
/// Starts monitoring the backend availability and publishes changes via <see cref="ConnectionStatusChanged"/>.
/// </summary>
/// <param name="token">Optional cancellation token to stop monitoring.</param>
public void Start(CancellationToken token = default)
{
ThrowIfDisposed();
Expand Down Expand Up @@ -57,6 +65,7 @@ public void Start(CancellationToken token = default)
}, _cts.Token);
}

/// <inheritdoc />
public void Dispose()
{
if (_disposed)
Expand Down
12 changes: 12 additions & 0 deletions src/Cryptie.Client/Core/Services/KeyService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ namespace Cryptie.Client.Core.Services;

public class KeyService(HttpClient httpClient) : IKeyService
{
/// <summary>
/// Retrieves the RSA public key for the specified user.
/// </summary>
/// <param name="getUserKeyRequest">Request containing the user identifier.</param>
/// <param name="cancellationToken">Token used to cancel the request.</param>
/// <returns>The user's key information or <c>null</c> when not found.</returns>
public async Task<GetUserKeyResponseDto?> GetUserKeyAsync(
GetUserKeyRequestDto getUserKeyRequest,
CancellationToken cancellationToken = default)
Expand All @@ -22,6 +28,12 @@ public class KeyService(HttpClient httpClient) : IKeyService
.ReadFromJsonAsync<GetUserKeyResponseDto>(cancellationToken);
}

/// <summary>
/// Retrieves symmetric keys for a collection of groups.
/// </summary>
/// <param name="getGroupsKeyRequest">Request specifying groups to retrieve.</param>
/// <param name="cancellationToken">Token used to cancel the request.</param>
/// <returns>Response containing keys for the requested groups.</returns>
public async Task<GetGroupsKeyResponseDto?> GetGroupsKeyAsync(
GetGroupsKeyRequestDto getGroupsKeyRequest,
CancellationToken cancellationToken = default)
Expand Down
15 changes: 15 additions & 0 deletions src/Cryptie.Client/Core/Services/UserDetailsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ namespace Cryptie.Client.Core.Services;

public class UserDetailsService(HttpClient httpClient) : IUserDetailsService
{
/// <summary>
/// Retrieves a user's display name based on their GUID.
/// </summary>
/// <param name="nameFromGuidRequest">Request containing the user GUID.</param>
/// <param name="cancellationToken">Token used to cancel the request.</param>
/// <returns>The user's display name or <c>null</c> when not found.</returns>
public async Task<NameFromGuidResponseDto?> GetUsernameFromGuidAsync(
NameFromGuidRequestDto nameFromGuidRequest,
CancellationToken cancellationToken = default)
Expand All @@ -21,6 +27,9 @@ public class UserDetailsService(HttpClient httpClient) : IUserDetailsService
return await response.Content.ReadFromJsonAsync<NameFromGuidResponseDto>(cancellationToken);
}

/// <summary>
/// Gets the user's GUID associated with a session token.
/// </summary>
public async Task<UserGuidFromTokenResponseDto?> GetUserGuidFromTokenAsync(
UserGuidFromTokenRequestDto userGuidFromTokenRequest,
CancellationToken cancellationToken = default)
Expand All @@ -34,6 +43,9 @@ public class UserDetailsService(HttpClient httpClient) : IUserDetailsService
return await response.Content.ReadFromJsonAsync<UserGuidFromTokenResponseDto>(cancellationToken);
}

/// <summary>
/// Retrieves the private key for the specified user.
/// </summary>
public async Task<UserPrivateKeyResponseDto?> GetUserPrivateKeyAsync(
UserPrivateKeyRequestDto userPrivateKeyRequest,
CancellationToken cancellationToken = default)
Expand All @@ -48,6 +60,9 @@ public class UserDetailsService(HttpClient httpClient) : IUserDetailsService
}


/// <summary>
/// Resolves a user's GUID from their login name.
/// </summary>
public async Task<GuidFromLoginResponseDto?> GetGuidFromLoginAsync(
GuidFromLoginRequestDto guidFromLoginRequest,
CancellationToken cancellationToken = default)
Expand Down
12 changes: 12 additions & 0 deletions src/Cryptie.Client/Encryption/AesDataEncryption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ namespace Cryptie.Client.Encryption;

public static class AesDataEncryption
{
/// <summary>
/// Encrypts the provided string with the given AES key.
/// </summary>
/// <param name="data">Plain text to encrypt.</param>
/// <param name="key">Base64 encoded AES key.</param>
/// <returns>Base64 encoded cipher text containing the IV and encrypted payload.</returns>
public static string Encrypt(string data, string key)
{
using var aes = Aes.Create();
Expand All @@ -26,6 +32,12 @@ public static string Encrypt(string data, string key)
return Convert.ToBase64String(ms.ToArray());
}

/// <summary>
/// Decrypts the given cipher text using the supplied AES key.
/// </summary>
/// <param name="encryptedData">Base64 encoded cipher that contains IV and encrypted data.</param>
/// <param name="key">Base64 encoded AES key.</param>
/// <returns>The decrypted plain text.</returns>
public static string Decrypt(string encryptedData, string key)
{
var fullCipher = Convert.FromBase64String(encryptedData);
Expand Down
9 changes: 9 additions & 0 deletions src/Cryptie.Client/Encryption/CertificateGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ namespace Cryptie.Client.Encryption;

public static class CertificateGenerator
{
/// <summary>
/// Generates a self-signed certificate that can be used for RSA encryption.
/// </summary>
/// <returns>A new <see cref="X509Certificate2" /> containing both private and public keys.</returns>
public static X509Certificate2 GenerateCertificate()
{
using var rsa = RSA.Create(2048);
Expand All @@ -23,6 +27,11 @@ public static X509Certificate2 GenerateCertificate()
DateTimeOffset.Now.AddYears(1));
}

/// <summary>
/// Extracts the public portion of the provided certificate.
/// </summary>
/// <param name="certificate">Certificate containing the key pair.</param>
/// <returns>A certificate containing only the public key.</returns>
public static X509Certificate2 ExtractPublicKey(X509Certificate2 certificate)
{
return X509CertificateLoader.LoadCertificate(certificate.Export(X509ContentType.Cert));
Expand Down
5 changes: 5 additions & 0 deletions src/Cryptie.Client/Encryption/EncryptionKeyGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ namespace Cryptie.Client.Encryption;

public static class EncryptionKeyGenerator
{
/// <summary>
/// Generates a new AES key with the specified key size.
/// </summary>
/// <param name="keySize">Size of the key in bits. Defaults to 256.</param>
/// <returns>Byte array containing the generated key.</returns>
public static byte[] GenerateAesKey(int keySize = 256)
{
using var aes = Aes.Create();
Expand Down
19 changes: 19 additions & 0 deletions src/Cryptie.Client/Encryption/RsaDataEncryption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ namespace Cryptie.Client.Encryption;

public static class RsaDataEncryption
{
/// <summary>
/// Encrypts the provided <paramref name="message" /> with the recipient's public key.
/// </summary>
/// <param name="message">Plain text message to encrypt.</param>
/// <param name="publicKey">Recipient's RSA public certificate.</param>
/// <returns>Base64 encoded encrypted payload.</returns>
public static string Encrypt(string message, X509Certificate2 publicKey)
{
var messageBytes = Encoding.UTF8.GetBytes(message);
Expand All @@ -19,6 +25,12 @@ public static string Encrypt(string message, X509Certificate2 publicKey)
return Convert.ToBase64String(envelopedCms.Encode());
}

/// <summary>
/// Decrypts an encrypted CMS message with the given private key.
/// </summary>
/// <param name="message">Base64 encoded encrypted data.</param>
/// <param name="privateKey">Certificate containing the private key used for decryption.</param>
/// <returns>The decrypted plain text message.</returns>
public static string Decrypt(string message, X509Certificate2 privateKey)
{
var messageBytes = Convert.FromBase64String(message);
Expand All @@ -30,6 +42,13 @@ public static string Decrypt(string message, X509Certificate2 privateKey)
return Encoding.UTF8.GetString(envelopedCms.ContentInfo.Content);
}

/// <summary>
/// Loads an <see cref="X509Certificate2" /> instance from a Base64 encoded string.
/// </summary>
/// <param name="base64">The Base64 encoded certificate.</param>
/// <param name="contentType">The format of the encoded certificate.</param>
/// <param name="password">Optional password for PFX certificates.</param>
/// <returns>The decoded certificate.</returns>
public static X509Certificate2 LoadCertificateFromBase64(string base64, X509ContentType contentType,
string? password = null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ namespace Cryptie.Client.Features.AddFriend.Services;

public class FriendsService(HttpClient httpClient) : IFriendsService
{
/// <summary>
/// Sends a friend request to the backend.
/// </summary>
/// <param name="addFriendRequest">Request DTO describing the friend to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task AddFriendAsync(AddFriendRequestDto addFriendRequest,
CancellationToken cancellationToken = default)
{
Expand Down
Loading
Loading