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 @@ -12,21 +12,36 @@ IDelayService delayService
)
: ControllerBase
{
[HttpPost("login", Name = "PostLogin")]
public async Task<IActionResult> Login([FromBody] LoginRequestDto loginRequest)
{
return await delayService.FakeDelay(() => authenticationService.LoginHandler(loginRequest));
}
[HttpPost("login", Name = "PostLogin")]
/// <summary>
/// Initiates the login process for a user.
/// </summary>
/// <param name="loginRequest">Credentials provided by the client.</param>
/// <returns>A task returning the authentication result.</returns>
public async Task<IActionResult> Login([FromBody] LoginRequestDto loginRequest)
{
return await delayService.FakeDelay(() => authenticationService.LoginHandler(loginRequest));
}

[HttpPost("totp", Name = "PostTotp")]
public async Task<IActionResult> Totp([FromBody] TotpRequestDto totpRequest)
{
return await delayService.FakeDelay(() => authenticationService.TotpHandler(totpRequest));
}
[HttpPost("totp", Name = "PostTotp")]
/// <summary>
/// Validates a TOTP code and returns a session token if successful.
/// </summary>
/// <param name="totpRequest">TOTP verification payload.</param>
/// <returns>A task returning the result of the verification.</returns>
public async Task<IActionResult> Totp([FromBody] TotpRequestDto totpRequest)
{
return await delayService.FakeDelay(() => authenticationService.TotpHandler(totpRequest));
}

[HttpPost("register", Name = "PostRegister")]
public async Task<IActionResult> Register([FromBody] RegisterRequestDto registerRequest)
{
return await delayService.FakeDelay(() => authenticationService.RegisterHandler(registerRequest));
}
[HttpPost("register", Name = "PostRegister")]
/// <summary>
/// Registers a new user in the system.
/// </summary>
/// <param name="registerRequest">User details and credentials.</param>
/// <returns>A task describing the outcome of the registration.</returns>
public async Task<IActionResult> Register([FromBody] RegisterRequestDto registerRequest)
{
return await delayService.FakeDelay(() => authenticationService.RegisterHandler(registerRequest));
}
}
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 @@ -11,15 +11,25 @@ public class GroupManagementController(
IGroupManagementService groupManagementService
) : ControllerBase
{
[HttpPost("isGroupsPrivate", Name = "IsGroupsPrivate")]
public IActionResult IsGroupsPrivate([FromBody] IsGroupsPrivateRequestDto isGroupsPrivateRequest)
{
return groupManagementService.IsGroupsPrivate(isGroupsPrivateRequest);
}
[HttpPost("isGroupsPrivate", Name = "IsGroupsPrivate")]
/// <summary>
/// Determines whether private groups are enabled on the server.
/// </summary>
/// <param name="isGroupsPrivateRequest">Request containing session token.</param>
/// <returns>True if private groups are enabled.</returns>
public IActionResult IsGroupsPrivate([FromBody] IsGroupsPrivateRequestDto isGroupsPrivateRequest)
{
return groupManagementService.IsGroupsPrivate(isGroupsPrivateRequest);
}

[HttpPost("groupsNames", Name = "GetGroupsNames")]
public IActionResult IsGroupsPrivate([FromBody] GetGroupsNamesRequestDto getGroupsNamesRequest)
{
return groupManagementService.GetGroupsNames(getGroupsNamesRequest);
}
[HttpPost("groupsNames", Name = "GetGroupsNames")]
/// <summary>
/// Returns display names of groups specified by their identifiers.
/// </summary>
/// <param name="getGroupsNamesRequest">Request with group ids.</param>
/// <returns>List of group names.</returns>
public IActionResult IsGroupsPrivate([FromBody] GetGroupsNamesRequestDto getGroupsNamesRequest)
{
return groupManagementService.GetGroupsNames(getGroupsNamesRequest);
}
}
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 @@ -8,15 +8,25 @@ namespace Cryptie.Server.Features.KeysManagement.Controllers;
[Route("keys")]
public class KeysManagementController(IKeysManagementService keysManagementService) : ControllerBase
{
[HttpGet("user")]
public IActionResult getUserKey([FromBody] GetUserKeyRequestDto getUserKeyRequest)
{
return keysManagementService.getUserKey(getUserKeyRequest);
}
[HttpGet("user")]
/// <summary>
/// Retrieves the symmetric key for the specified user.
/// </summary>
/// <param name="getUserKeyRequest">Request describing the user.</param>
/// <returns>The encryption key if found.</returns>
public IActionResult getUserKey([FromBody] GetUserKeyRequestDto getUserKeyRequest)
{
return keysManagementService.getUserKey(getUserKeyRequest);
}

[HttpGet("groupsKey", Name = "GetGroupsKey")]
public IActionResult getGroupKey([FromBody] GetGroupsKeyRequestDto getGroupsKeyRequest)
{
return keysManagementService.getGroupsKey(getGroupsKeyRequest);
}
[HttpGet("groupsKey", Name = "GetGroupsKey")]
/// <summary>
/// Returns the encryption key used for a group conversation.
/// </summary>
/// <param name="getGroupsKeyRequest">Information about the group.</param>
/// <returns>Key material for the group.</returns>
public IActionResult getGroupKey([FromBody] GetGroupsKeyRequestDto getGroupsKeyRequest)
{
return keysManagementService.getGroupsKey(getGroupsKeyRequest);
}
}
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 @@ -9,21 +9,36 @@ namespace Cryptie.Server.Features.Messages.Controllers;
public class MessagesController(IMessagesService messagesService)
: ControllerBase
{
[HttpPost("send", Name = "PostSendMessage")]
public IActionResult SendMessage([FromBody] SendMessageRequestDto sendMessageRequest)
{
return messagesService.SendMessage(sendMessageRequest);
}
[HttpPost("send", Name = "PostSendMessage")]
/// <summary>
/// Sends an encrypted message to a group.
/// </summary>
/// <param name="sendMessageRequest">Message payload including group and text.</param>
/// <returns>HTTP result from the messaging service.</returns>
public IActionResult SendMessage([FromBody] SendMessageRequestDto sendMessageRequest)
{
return messagesService.SendMessage(sendMessageRequest);
}

[HttpGet("get-all", Name = "GetGroupMessages")]
public IActionResult GetGroupMessages([FromBody] GetGroupMessagesRequestDto groupMessagesRequest)
{
return messagesService.GetGroupMessages(groupMessagesRequest);
}
[HttpGet("get-all", Name = "GetGroupMessages")]
/// <summary>
/// Retrieves all messages for a specified group.
/// </summary>
/// <param name="groupMessagesRequest">Request identifying the group.</param>
/// <returns>Collection of messages.</returns>
public IActionResult GetGroupMessages([FromBody] GetGroupMessagesRequestDto groupMessagesRequest)
{
return messagesService.GetGroupMessages(groupMessagesRequest);
}

[HttpPost("get-all-since", Name = "GetGroupMessagesSince")]
public IActionResult GetGroupMessagesSince([FromBody] GetGroupMessagesSinceRequestDto getGroupMessagesSinceRequest)
{
return messagesService.GetGroupMessagesSince(getGroupMessagesSinceRequest);
}
[HttpPost("get-all-since", Name = "GetGroupMessagesSince")]
/// <summary>
/// Returns messages added to the group after a specific timestamp.
/// </summary>
/// <param name="getGroupMessagesSinceRequest">Request containing group identifier and timestamp.</param>
/// <returns>List of recent messages.</returns>
public IActionResult GetGroupMessagesSince([FromBody] GetGroupMessagesSinceRequestDto getGroupMessagesSinceRequest)
{
return messagesService.GetGroupMessagesSince(getGroupMessagesSinceRequest);
}
}
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
Loading
Loading