Skip to content
Closed
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
4 changes: 4 additions & 0 deletions src/Cryptie.Server/DatabaseUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ public DatabaseUpdater(IServiceProvider serviceProvider)
_serviceProvider = serviceProvider;
}

/// <summary>
/// Applies pending Entity Framework migrations on application startup.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task PerformDatabaseUpdate()
{
using var scope = _serviceProvider.CreateScope();
Expand Down
4 changes: 4 additions & 0 deletions src/Cryptie.Server/DockerStarter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ namespace Cryptie.Server;

public class DockerStarter
{
/// <summary>
/// Starts a local PostgreSQL container if it is not running.
/// </summary>
/// <returns>A task representing the asynchronous startup process.</returns>
public async Task StartPostgresAsync()
{
var client = new DockerClientConfiguration().CreateClient();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ public class AuthenticationService(
IDatabaseService databaseService)
: ControllerBase, IAuthenticationService
{
/// <summary>
/// Handles the first phase of login by verifying provided credentials
/// and issuing a temporary TOTP token when successful.
/// </summary>
/// <param name="loginRequest">Login credentials.</param>
/// <returns>
/// <see cref="LoginResponseDto"/> containing a TOTP token when
/// credentials are valid; otherwise an error result.
/// </returns>
public IActionResult LoginHandler(LoginRequestDto loginRequest)
{
var user = appDbContext.Users
Expand All @@ -40,6 +49,12 @@ public IActionResult LoginHandler(LoginRequestDto loginRequest)
return Ok(new LoginResponseDto { TotpToken = totpToken });
}

/// <summary>
/// Validates the provided TOTP code and issues a session token when
/// the code is correct.
/// </summary>
/// <param name="totpRequest">Request containing TOTP code and token.</param>
/// <returns>Session token on success or an error result.</returns>
public IActionResult TotpHandler(TotpRequestDto totpRequest)
{
var now = DateTime.UtcNow;
Expand Down Expand Up @@ -72,6 +87,12 @@ public IActionResult TotpHandler(TotpRequestDto totpRequest)
});
}

/// <summary>
/// Registers a new user and returns the initial TOTP configuration
/// along with a TOTP token for verification.
/// </summary>
/// <param name="registerRequest">Information required to create the user.</param>
/// <returns>Registration details including the TOTP secret.</returns>
public IActionResult RegisterHandler(RegisterRequestDto registerRequest)
{
if (databaseService.FindUserByLogin(registerRequest.Login) != null) return BadRequest();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ public class DelayService : IDelayService
{
private const int TargetMilliseconds = 100;

/// <summary>
/// Executes the given action and ensures that the call takes at least a
/// predefined amount of time. Useful to mitigate timing attacks.
/// </summary>
/// <param name="func">Action returning an <see cref="IActionResult"/> to execute.</param>
/// <returns>Result of <paramref name="func"/> after the artificial delay.</returns>
public async Task<IActionResult> FakeDelay(Func<IActionResult> func)
{
var stopwatch = Stopwatch.StartNew();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ namespace Cryptie.Server.Features.Authentication.Services;

public class LockoutService(IAppDbContext appDbContext) : ILockoutService
{
/// <summary>
/// Determines whether a user or honeypot account should be locked out
/// based on failed login attempts.
/// </summary>
/// <param name="user">Real user instance if available.</param>
/// <param name="honeypotLogin">Login of a honeypot account when no real user exists.</param>
/// <returns><c>true</c> if the account is locked out; otherwise <c>false</c>.</returns>
public bool IsUserLockedOut(User? user, string honeypotLogin = "")
{
var referenceLockTimestamp = DateTime.UtcNow.AddMinutes(-60);
Expand All @@ -28,28 +35,56 @@ public bool IsUserLockedOut(User? user, string honeypotLogin = "")
return true;
}

/// <summary>
/// Checks if the specified user has an active lockout entry.
/// </summary>
/// <param name="user">User entity to check.</param>
/// <param name="referenceLockTimestamp">Timestamp defining the lockout window.</param>
/// <returns><c>true</c> when the user has an active lock.</returns>
public bool IsUserAccountHasLock(User user, DateTime referenceLockTimestamp)
{
return appDbContext.UserAccountLocks.Any(l => l.User == user && l.Until > referenceLockTimestamp);
}

/// <summary>
/// Checks if the honeypot account has an active lockout entry.
/// </summary>
/// <param name="user">Honeypot login to check.</param>
/// <param name="referenceLockTimestamp">Timestamp defining the lockout window.</param>
/// <returns><c>true</c> when the account has an active lock.</returns>
public bool IsUserAccountHasLock(string user, DateTime referenceLockTimestamp)
{
return appDbContext.HoneypotAccountLocks.Any(l => l.Username == user && l.Until > referenceLockTimestamp);
}

/// <summary>
/// Evaluates if the user has exceeded the allowed login attempts.
/// </summary>
/// <param name="user">User entity to check.</param>
/// <param name="referenceAttemptTimestamp">Timestamp defining the attempts window.</param>
/// <returns><c>true</c> if there are too many attempts.</returns>
public bool IsUserAccountHasTooManyAttempts(User user, DateTime referenceAttemptTimestamp)
{
return appDbContext.UserLoginAttempts.Count(a => a.User == user && a.Timestamp > referenceAttemptTimestamp) <
2;
}

/// <summary>
/// Evaluates if the honeypot account has exceeded the allowed login attempts.
/// </summary>
/// <param name="user">Honeypot login to check.</param>
/// <param name="referenceAttemptTimestamp">Timestamp defining the attempts window.</param>
/// <returns><c>true</c> if there are too many attempts.</returns>
public bool IsUserAccountHasTooManyAttempts(string user, DateTime referenceAttemptTimestamp)
{
return appDbContext.HoneypotLoginAttempts.Count(a =>
a.Username == user && a.Timestamp > referenceAttemptTimestamp) < 2;
}

/// <summary>
/// Creates a lockout entry for the specified user.
/// </summary>
/// <param name="user">User entity to lock.</param>
public void LockUserAccount(User user)
{
appDbContext.UserAccountLocks.Add(new UserAccountLock
Expand All @@ -62,6 +97,10 @@ public void LockUserAccount(User user)
appDbContext.SaveChanges();
}

/// <summary>
/// Creates a lockout entry for the specified honeypot login.
/// </summary>
/// <param name="user">Honeypot login to lock.</param>
public void LockUserAccount(string user)
{
appDbContext.HoneypotAccountLocks.Add(new HoneypotAccountLock
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ public class GroupManagementService(
IDatabaseService databaseService
) : ControllerBase, IGroupManagementService
{
/// <summary>
/// Determines privacy status for a collection of groups.
/// </summary>
/// <param name="isGroupsPrivateRequest">List of group identifiers.</param>
/// <returns>Dictionary with group ids and their privacy status.</returns>
public IActionResult IsGroupsPrivate(IsGroupsPrivateRequestDto isGroupsPrivateRequest)
{
var result = new Dictionary<Guid, bool>();
Expand All @@ -22,6 +27,12 @@ public IActionResult IsGroupsPrivate(IsGroupsPrivateRequestDto isGroupsPrivateRe
return Ok(new IsGroupsPrivateResponseDto { GroupStatuses = result });
}

/// <summary>
/// Returns display names for all groups the user is a member of.
/// Private groups are represented by the other member's display name.
/// </summary>
/// <param name="getGroupsNamesRequest">Request containing user session token.</param>
/// <returns>Mapping of group ids to their display names.</returns>
public IActionResult GetGroupsNames([FromBody] GetGroupsNamesRequestDto getGroupsNamesRequest)
{
var user = databaseService.GetUserFromToken(getGroupsNamesRequest.SessionToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ namespace Cryptie.Server.Features.KeysManagement.Services;

public class KeysManagementService(IDatabaseService databaseService) : ControllerBase, IKeysManagementService
{
/// <summary>
/// Retrieves public key of specified user.
/// </summary>
/// <param name="getUserKeyRequest">Request containing the user identifier.</param>
/// <returns>DTO with the public key string.</returns>
public IActionResult getUserKey([FromBody] GetUserKeyRequestDto getUserKeyRequest)
{
var key = databaseService.GetUserPublicKey(getUserKeyRequest.UserId);
Expand All @@ -15,6 +20,11 @@ public IActionResult getUserKey([FromBody] GetUserKeyRequestDto getUserKeyReques
});
}

/// <summary>
/// Retrieves AES keys for all groups the current user belongs to.
/// </summary>
/// <param name="getGroupsKeyRequest">Request containing the session token.</param>
/// <returns>Dictionary mapping group ids to their AES keys.</returns>
public IActionResult getGroupsKey([FromBody] GetGroupsKeyRequestDto getGroupsKeyRequest)
{
var user = databaseService.GetUserFromToken(getGroupsKeyRequest.SessionToken);
Expand Down
15 changes: 15 additions & 0 deletions src/Cryptie.Server/Features/Messages/Hubs/MessageHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,33 @@ public class MessageHub : Hub, IMessageHub
{
private static readonly ConcurrentDictionary<string, Guid> Users = new();

/// <summary>
/// Broadcasts a plain text message to all connected clients.
/// </summary>
/// <param name="message">Message to broadcast.</param>
public async Task SendMessage(string message)
{
await Clients.All.SendAsync("ReceiveMessage", message);
}

/// <summary>
/// Adds the current connection to a SignalR group and notifies other members.
/// </summary>
/// <param name="user">Identifier of the joining user.</param>
/// <param name="group">Group identifier.</param>
public async Task JoinGroup(Guid user, Guid group)
{
Users.TryAdd(Context.ConnectionId, user);
await Groups.AddToGroupAsync(Context.ConnectionId, group.ToString());
await Clients.Group(group.ToString()).SendAsync("UserJoinedGroup", user, group);
}

/// <summary>
/// Sends a message to all members of a group.
/// </summary>
/// <param name="group">Group identifier.</param>
/// <param name="senderId">Sender identifier.</param>
/// <param name="message">Encrypted message payload.</param>
public async Task SendMessageToGroup(Guid group, Guid senderId, string message)
{
await Clients
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ public MessageHubService(IHubContext<MessageHub> hubContext)
_hubContext = hubContext;
}

/// <summary>
/// Sends a real-time notification to all clients connected to the specified group.
/// </summary>
/// <param name="group">Group identifier.</param>
/// <param name="senderId">Identifier of the message sender.</param>
/// <param name="message">Encrypted message body.</param>
public void SendMessageToGroup(Guid group, Guid senderId, string message)
{
_hubContext.Clients.Group(group.ToString())
Expand Down
15 changes: 15 additions & 0 deletions src/Cryptie.Server/Features/Messages/Services/MessagesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ namespace Cryptie.Server.Features.Messages.Services;
public class MessagesService(IDatabaseService databaseService, IMessageHubService messageHubService)
: ControllerBase, IMessagesService
{
/// <summary>
/// Sends a message to a specified group on behalf of the authenticated user.
/// </summary>
/// <param name="sendMessageRequest">Request containing message and authentication data.</param>
/// <returns><see cref="OkResult"/> when the message is sent.</returns>
public IActionResult SendMessage([FromBody] SendMessageRequestDto sendMessageRequest)
{
var user = databaseService.GetUserFromToken(sendMessageRequest.SenderToken);
Expand All @@ -24,6 +29,11 @@ public IActionResult SendMessage([FromBody] SendMessageRequestDto sendMessageReq
return Ok();
}

/// <summary>
/// Retrieves all messages for a particular group.
/// </summary>
/// <param name="request">Request containing user token and group identifier.</param>
/// <returns>List of messages.</returns>
public IActionResult GetGroupMessages([FromBody] GetGroupMessagesRequestDto request)
{
var user = databaseService.GetUserFromToken(request.UserToken);
Expand All @@ -49,6 +59,11 @@ public IActionResult GetGroupMessages([FromBody] GetGroupMessagesRequestDto requ
return Ok(response);
}

/// <summary>
/// Retrieves messages for a group that were sent after the specified timestamp.
/// </summary>
/// <param name="request">Request containing group id, token and timestamp.</param>
/// <returns>List of messages sent since the provided date.</returns>
public IActionResult GetGroupMessagesSince([FromBody] GetGroupMessagesSinceRequestDto request)
{
var user = databaseService.GetUserFromToken(request.UserToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ namespace Cryptie.Server.Features.ServerStatus.Services;

public class ServerStatusService : ControllerBase, IServerStatusService
{
/// <summary>
/// Simple health-check endpoint.
/// </summary>
/// <returns><see cref="OkResult"/> when the server is running.</returns>
public IActionResult GetServerStatus()
{
return Ok();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ namespace Cryptie.Server.Features.UserManagement.Services;

public class UserManagementService(IDatabaseService databaseService) : ControllerBase, IUserManagementService
{
/// <summary>
/// Returns user GUID associated with a session token.
/// </summary>
/// <param name="userGuidFromTokenRequest">Request containing session token.</param>
/// <returns>User identifier on success.</returns>
public IActionResult UserGuidFromToken([FromBody] UserGuidFromTokenRequestDto userGuidFromTokenRequest)
{
var user = databaseService.GetUserFromToken(userGuidFromTokenRequest.SessionToken);
Expand All @@ -17,6 +22,11 @@ public IActionResult UserGuidFromToken([FromBody] UserGuidFromTokenRequestDto us
});
}

/// <summary>
/// Adds a user to the current user's friend list and creates a private group for them.
/// </summary>
/// <param name="addFriendRequest">Request containing friend login and encryption keys.</param>
/// <returns><see cref="OkResult"/> when the friend was added.</returns>
public IActionResult AddFriend([FromBody] AddFriendRequestDto addFriendRequest)
{
var friend = databaseService.FindUserByLogin(addFriendRequest.Friend);
Expand Down Expand Up @@ -44,6 +54,11 @@ public IActionResult AddFriend([FromBody] AddFriendRequestDto addFriendRequest)
return Ok();
}

/// <summary>
/// Retrieves the display name of a user by their GUID.
/// </summary>
/// <param name="nameFromGuidRequest">Request with the user's id.</param>
/// <returns>Display name of the user.</returns>
public IActionResult NameFromGuid([FromBody] NameFromGuidRequestDto nameFromGuidRequest)
{
var user = databaseService.FindUserById(nameFromGuidRequest.Id);
Expand All @@ -55,6 +70,11 @@ public IActionResult NameFromGuid([FromBody] NameFromGuidRequestDto nameFromGuid
});
}

/// <summary>
/// Returns identifiers of groups the user belongs to.
/// </summary>
/// <param name="userGroupsRequest">Request containing session token.</param>
/// <returns>List of group identifiers.</returns>
public IActionResult UserGroups([FromBody] UserGroupsRequestDto userGroupsRequest)
{
var user = databaseService.GetUserFromToken(userGroupsRequest.SessionToken);
Expand All @@ -68,6 +88,11 @@ public IActionResult UserGroups([FromBody] UserGroupsRequestDto userGroupsReques
});
}

/// <summary>
/// Changes the display name of the authenticated user.
/// </summary>
/// <param name="userDisplayNameRequest">Request containing token and new name.</param>
/// <returns><see cref="OkResult"/> when the change was applied.</returns>
public IActionResult UserDisplayName([FromBody] UserDisplayNameRequestDto userDisplayNameRequest)
{
var user = databaseService.GetUserFromToken(userDisplayNameRequest.Token);
Expand All @@ -78,6 +103,11 @@ public IActionResult UserDisplayName([FromBody] UserDisplayNameRequestDto userDi
return Ok();
}

/// <summary>
/// Returns the user's private key used for end-to-end encryption.
/// </summary>
/// <param name="userPrivateKeyRequest">Request containing session token.</param>
/// <returns>Private key and associated control value.</returns>
public IActionResult UserPrivateKey([FromBody] UserPrivateKeyRequestDto userPrivateKeyRequest)
{
var user = databaseService.GetUserFromToken(userPrivateKeyRequest.SessionToken);
Expand All @@ -90,6 +120,11 @@ public IActionResult UserPrivateKey([FromBody] UserPrivateKeyRequestDto userPriv
});
}

/// <summary>
/// Retrieves the unique identifier of a user from their login name.
/// </summary>
/// <param name="guidFromLoginRequest">Request containing user login.</param>
/// <returns>User identifier when found.</returns>
public IActionResult GuidFromLogin([FromBody] GuidFromLoginRequestDto guidFromLoginRequest)
{
var user = databaseService.FindUserByLogin(guidFromLoginRequest.Login);
Expand Down
Loading
Loading