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

/// <summary>
/// Applies any pending Entity Framework migrations to ensure the database
/// schema is up to date.
/// </summary>
/// <returns>A task that completes when the migration process finishes.</returns>
public async Task PerformDatabaseUpdate()
{
using var scope = _serviceProvider.CreateScope();
Expand Down
6 changes: 6 additions & 0 deletions src/Cryptie.Server/DockerStarter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ namespace Cryptie.Server;

public class DockerStarter
{
/// <summary>
/// Ensures that a PostgreSQL container is running for the application.
/// If an existing container named <c>cryptie-db</c> is found it will be started
/// if necessary, otherwise a new one will be created and started.
/// </summary>
/// <returns>A task representing the asynchronous start operation.</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,12 @@ public class AuthenticationService(
IDatabaseService databaseService)
: ControllerBase, IAuthenticationService
{
/// <summary>
/// Handles user login by validating credentials and issuing a TOTP token
/// when successful.
/// </summary>
/// <param name="loginRequest">Login credentials.</param>
/// <returns>An <see cref="IActionResult"/> describing the outcome.</returns>
public IActionResult LoginHandler(LoginRequestDto loginRequest)
{
var user = appDbContext.Users
Expand All @@ -40,6 +46,11 @@ public IActionResult LoginHandler(LoginRequestDto loginRequest)
return Ok(new LoginResponseDto { TotpToken = totpToken });
}

/// <summary>
/// Validates a TOTP code and returns a session token if it is correct.
/// </summary>
/// <param name="totpRequest">User TOTP request.</param>
/// <returns>An <see cref="IActionResult"/> describing the outcome.</returns>
public IActionResult TotpHandler(TotpRequestDto totpRequest)
{
var now = DateTime.UtcNow;
Expand Down Expand Up @@ -72,6 +83,11 @@ public IActionResult TotpHandler(TotpRequestDto totpRequest)
});
}

/// <summary>
/// Registers a new user and returns provisioning information for TOTP.
/// </summary>
/// <param name="registerRequest">Registration details.</param>
/// <returns>An <see cref="IActionResult"/> with the registration result.</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,13 @@ public class DelayService : IDelayService
{
private const int TargetMilliseconds = 100;

/// <summary>
/// Executes the provided action ensuring that the total execution time is at least
/// <see cref="TargetMilliseconds"/> milliseconds. This is used to mitigate timing
/// attacks on authentication endpoints.
/// </summary>
/// <param name="func">Action that produces the result.</param>
/// <returns>The result of <paramref name="func"/> after the enforced 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 the specified user or honeypot account should be locked out
/// based on recent login attempts.
/// </summary>
/// <param name="user">The real user to check.</param>
/// <param name="honeypotLogin">Login string used when user is null.</param>
/// <returns><c>true</c> if access should be denied.</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 whether a user account currently has an active lock.
/// </summary>
/// <param name="user">User account.</param>
/// <param name="referenceLockTimestamp">Timestamp of oldest valid lock.</param>
/// <returns><c>true</c> if a lock exists.</returns>
public bool IsUserAccountHasLock(User user, DateTime referenceLockTimestamp)
{
return appDbContext.UserAccountLocks.Any(l => l.User == user && l.Until > referenceLockTimestamp);
}

/// <summary>
/// Checks whether a honeypot login has an active lock.
/// </summary>
/// <param name="user">The honeypot login name.</param>
/// <param name="referenceLockTimestamp">Timestamp of oldest valid lock.</param>
/// <returns><c>true</c> if locked.</returns>
public bool IsUserAccountHasLock(string user, DateTime referenceLockTimestamp)
{
return appDbContext.HoneypotAccountLocks.Any(l => l.Username == user && l.Until > referenceLockTimestamp);
}

/// <summary>
/// Checks if a user has exceeded the allowed number of login attempts.
/// </summary>
/// <param name="user">User account.</param>
/// <param name="referenceAttemptTimestamp">Only attempts newer than this are counted.</param>
/// <returns><c>true</c> when attempts are within limit.</returns>
public bool IsUserAccountHasTooManyAttempts(User user, DateTime referenceAttemptTimestamp)
{
return appDbContext.UserLoginAttempts.Count(a => a.User == user && a.Timestamp > referenceAttemptTimestamp) <
2;
}

/// <summary>
/// Checks if a honeypot login has too many login attempts.
/// </summary>
/// <param name="user">The honeypot login name.</param>
/// <param name="referenceAttemptTimestamp">Only attempts newer than this are counted.</param>
/// <returns><c>true</c> when attempts are within limit.</returns>
public bool IsUserAccountHasTooManyAttempts(string user, DateTime referenceAttemptTimestamp)
{
return appDbContext.HoneypotLoginAttempts.Count(a =>
a.Username == user && a.Timestamp > referenceAttemptTimestamp) < 2;
}

/// <summary>
/// Adds a lock entry for the specified user.
/// </summary>
/// <param name="user">User 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>
/// Adds a lock entry for a honeypot login.
/// </summary>
/// <param name="user">Login string 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>
/// Returns privacy flags for the requested groups.
/// </summary>
/// <param name="isGroupsPrivateRequest">Request containing group identifiers.</param>
/// <returns>Dictionary of group ids with their privacy status.</returns>
public IActionResult IsGroupsPrivate(IsGroupsPrivateRequestDto isGroupsPrivateRequest)
{
var result = new Dictionary<Guid, bool>();
Expand All @@ -22,6 +27,11 @@ public IActionResult IsGroupsPrivate(IsGroupsPrivateRequestDto isGroupsPrivateRe
return Ok(new IsGroupsPrivateResponseDto { GroupStatuses = result });
}

/// <summary>
/// Gets display names for all groups that a user is a member of.
/// </summary>
/// <param name="getGroupsNamesRequest">Request including the session token.</param>
/// <returns>Mapping of group ids to 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 the public key of a specific user.
/// </summary>
/// <param name="getUserKeyRequest">Request containing the user identifier.</param>
/// <returns>The user's public key.</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>
/// Gets encryption keys for all groups the requesting user is part of.
/// </summary>
/// <param name="getGroupsKeyRequest">Request containing the session token.</param>
/// <returns>Dictionary of group ids with their encryption keys.</returns>
public IActionResult getGroupsKey([FromBody] GetGroupsKeyRequestDto getGroupsKeyRequest)
{
var user = databaseService.GetUserFromToken(getGroupsKeyRequest.SessionToken);
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 message to all clients in the specified SignalR group.
/// </summary>
/// <param name="group">Group identifier.</param>
/// <param name="senderId">User sending the message.</param>
/// <param name="message">Encrypted message content.</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 group on behalf of the authenticated user.
/// </summary>
/// <param name="sendMessageRequest">Request containing the session token, target group and message.</param>
/// <returns>Status of the send operation.</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 from a specific group.
/// </summary>
/// <param name="request">Request containing the user token and group id.</param>
/// <returns>A list of messages in the group.</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 from a group posted after the specified timestamp.
/// </summary>
/// <param name="request">Request containing group id, token and timestamp.</param>
/// <returns>List of messages newer than 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 confirming that the server is running.
/// </summary>
/// <returns>HTTP 200 response.</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>
/// Resolves a user identifier from a valid session token.
/// </summary>
/// <param name="userGuidFromTokenRequest">Request containing the token.</param>
/// <returns>User id when the token is valid.</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>
/// Creates a private group for two users and stores provided encryption keys.
/// </summary>
/// <param name="addFriendRequest">Information about the friend and keys.</param>
/// <returns>Result of the operation.</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>
/// Returns the display name of a user identified by GUID.
/// </summary>
/// <param name="nameFromGuidRequest">Request containing the user id.</param>
/// <returns>User display name.</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 the session token.</param>
/// <returns>List of group ids.</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 for the authenticated user.
/// </summary>
/// <param name="userDisplayNameRequest">Request containing token and new name.</param>
/// <returns>HTTP 200 on success.</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>
/// Retrieves the private key and control value of a user.
/// </summary>
/// <param name="userPrivateKeyRequest">Request containing session token.</param>
/// <returns>The private key pair.</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>
/// Finds a user identifier by login.
/// </summary>
/// <param name="guidFromLoginRequest">Request containing the login.</param>
/// <returns>User id or 404.</returns>
public IActionResult GuidFromLogin([FromBody] GuidFromLoginRequestDto guidFromLoginRequest)
{
var user = databaseService.FindUserByLogin(guidFromLoginRequest.Login);
Expand Down
Loading
Loading