diff --git a/src/Cryptie.Server/DatabaseUpdater.cs b/src/Cryptie.Server/DatabaseUpdater.cs index 9515303..c8450d6 100644 --- a/src/Cryptie.Server/DatabaseUpdater.cs +++ b/src/Cryptie.Server/DatabaseUpdater.cs @@ -12,6 +12,11 @@ public DatabaseUpdater(IServiceProvider serviceProvider) _serviceProvider = serviceProvider; } + /// + /// Applies any pending Entity Framework migrations to ensure the database + /// schema is up to date. + /// + /// A task that completes when the migration process finishes. public async Task PerformDatabaseUpdate() { using var scope = _serviceProvider.CreateScope(); diff --git a/src/Cryptie.Server/DockerStarter.cs b/src/Cryptie.Server/DockerStarter.cs index e658c74..4fef428 100644 --- a/src/Cryptie.Server/DockerStarter.cs +++ b/src/Cryptie.Server/DockerStarter.cs @@ -5,6 +5,12 @@ namespace Cryptie.Server; public class DockerStarter { + /// + /// Ensures that a PostgreSQL container is running for the application. + /// If an existing container named cryptie-db is found it will be started + /// if necessary, otherwise a new one will be created and started. + /// + /// A task representing the asynchronous start operation. public async Task StartPostgresAsync() { var client = new DockerClientConfiguration().CreateClient(); diff --git a/src/Cryptie.Server/Features/Authentication/Services/AuthenticationService.cs b/src/Cryptie.Server/Features/Authentication/Services/AuthenticationService.cs index e1abdfd..56b26da 100644 --- a/src/Cryptie.Server/Features/Authentication/Services/AuthenticationService.cs +++ b/src/Cryptie.Server/Features/Authentication/Services/AuthenticationService.cs @@ -15,6 +15,12 @@ public class AuthenticationService( IDatabaseService databaseService) : ControllerBase, IAuthenticationService { + /// + /// Handles user login by validating credentials and issuing a TOTP token + /// when successful. + /// + /// Login credentials. + /// An describing the outcome. public IActionResult LoginHandler(LoginRequestDto loginRequest) { var user = appDbContext.Users @@ -40,6 +46,11 @@ public IActionResult LoginHandler(LoginRequestDto loginRequest) return Ok(new LoginResponseDto { TotpToken = totpToken }); } + /// + /// Validates a TOTP code and returns a session token if it is correct. + /// + /// User TOTP request. + /// An describing the outcome. public IActionResult TotpHandler(TotpRequestDto totpRequest) { var now = DateTime.UtcNow; @@ -72,6 +83,11 @@ public IActionResult TotpHandler(TotpRequestDto totpRequest) }); } + /// + /// Registers a new user and returns provisioning information for TOTP. + /// + /// Registration details. + /// An with the registration result. public IActionResult RegisterHandler(RegisterRequestDto registerRequest) { if (databaseService.FindUserByLogin(registerRequest.Login) != null) return BadRequest(); diff --git a/src/Cryptie.Server/Features/Authentication/Services/DelayService.cs b/src/Cryptie.Server/Features/Authentication/Services/DelayService.cs index a977c3d..72566e0 100644 --- a/src/Cryptie.Server/Features/Authentication/Services/DelayService.cs +++ b/src/Cryptie.Server/Features/Authentication/Services/DelayService.cs @@ -7,6 +7,13 @@ public class DelayService : IDelayService { private const int TargetMilliseconds = 100; + /// + /// Executes the provided action ensuring that the total execution time is at least + /// milliseconds. This is used to mitigate timing + /// attacks on authentication endpoints. + /// + /// Action that produces the result. + /// The result of after the enforced delay. public async Task FakeDelay(Func func) { var stopwatch = Stopwatch.StartNew(); diff --git a/src/Cryptie.Server/Features/Authentication/Services/LockoutService.cs b/src/Cryptie.Server/Features/Authentication/Services/LockoutService.cs index dc72f65..70286b7 100644 --- a/src/Cryptie.Server/Features/Authentication/Services/LockoutService.cs +++ b/src/Cryptie.Server/Features/Authentication/Services/LockoutService.cs @@ -5,6 +5,13 @@ namespace Cryptie.Server.Features.Authentication.Services; public class LockoutService(IAppDbContext appDbContext) : ILockoutService { + /// + /// Determines whether the specified user or honeypot account should be locked out + /// based on recent login attempts. + /// + /// The real user to check. + /// Login string used when user is null. + /// true if access should be denied. public bool IsUserLockedOut(User? user, string honeypotLogin = "") { var referenceLockTimestamp = DateTime.UtcNow.AddMinutes(-60); @@ -28,28 +35,56 @@ public bool IsUserLockedOut(User? user, string honeypotLogin = "") return true; } + /// + /// Checks whether a user account currently has an active lock. + /// + /// User account. + /// Timestamp of oldest valid lock. + /// true if a lock exists. public bool IsUserAccountHasLock(User user, DateTime referenceLockTimestamp) { return appDbContext.UserAccountLocks.Any(l => l.User == user && l.Until > referenceLockTimestamp); } + /// + /// Checks whether a honeypot login has an active lock. + /// + /// The honeypot login name. + /// Timestamp of oldest valid lock. + /// true if locked. public bool IsUserAccountHasLock(string user, DateTime referenceLockTimestamp) { return appDbContext.HoneypotAccountLocks.Any(l => l.Username == user && l.Until > referenceLockTimestamp); } + /// + /// Checks if a user has exceeded the allowed number of login attempts. + /// + /// User account. + /// Only attempts newer than this are counted. + /// true when attempts are within limit. public bool IsUserAccountHasTooManyAttempts(User user, DateTime referenceAttemptTimestamp) { return appDbContext.UserLoginAttempts.Count(a => a.User == user && a.Timestamp > referenceAttemptTimestamp) < 2; } + /// + /// Checks if a honeypot login has too many login attempts. + /// + /// The honeypot login name. + /// Only attempts newer than this are counted. + /// true when attempts are within limit. public bool IsUserAccountHasTooManyAttempts(string user, DateTime referenceAttemptTimestamp) { return appDbContext.HoneypotLoginAttempts.Count(a => a.Username == user && a.Timestamp > referenceAttemptTimestamp) < 2; } + /// + /// Adds a lock entry for the specified user. + /// + /// User to lock. public void LockUserAccount(User user) { appDbContext.UserAccountLocks.Add(new UserAccountLock @@ -62,6 +97,10 @@ public void LockUserAccount(User user) appDbContext.SaveChanges(); } + /// + /// Adds a lock entry for a honeypot login. + /// + /// Login string to lock. public void LockUserAccount(string user) { appDbContext.HoneypotAccountLocks.Add(new HoneypotAccountLock diff --git a/src/Cryptie.Server/Features/GroupManagement/Services/GroupManagementService.cs b/src/Cryptie.Server/Features/GroupManagement/Services/GroupManagementService.cs index 6686cc1..da9dbe3 100644 --- a/src/Cryptie.Server/Features/GroupManagement/Services/GroupManagementService.cs +++ b/src/Cryptie.Server/Features/GroupManagement/Services/GroupManagementService.cs @@ -9,6 +9,11 @@ public class GroupManagementService( IDatabaseService databaseService ) : ControllerBase, IGroupManagementService { + /// + /// Returns privacy flags for the requested groups. + /// + /// Request containing group identifiers. + /// Dictionary of group ids with their privacy status. public IActionResult IsGroupsPrivate(IsGroupsPrivateRequestDto isGroupsPrivateRequest) { var result = new Dictionary(); @@ -22,6 +27,11 @@ public IActionResult IsGroupsPrivate(IsGroupsPrivateRequestDto isGroupsPrivateRe return Ok(new IsGroupsPrivateResponseDto { GroupStatuses = result }); } + /// + /// Gets display names for all groups that a user is a member of. + /// + /// Request including the session token. + /// Mapping of group ids to display names. public IActionResult GetGroupsNames([FromBody] GetGroupsNamesRequestDto getGroupsNamesRequest) { var user = databaseService.GetUserFromToken(getGroupsNamesRequest.SessionToken); diff --git a/src/Cryptie.Server/Features/KeysManagement/Services/KeysManagementService.cs b/src/Cryptie.Server/Features/KeysManagement/Services/KeysManagementService.cs index 7cf98ec..51087de 100644 --- a/src/Cryptie.Server/Features/KeysManagement/Services/KeysManagementService.cs +++ b/src/Cryptie.Server/Features/KeysManagement/Services/KeysManagementService.cs @@ -6,6 +6,11 @@ namespace Cryptie.Server.Features.KeysManagement.Services; public class KeysManagementService(IDatabaseService databaseService) : ControllerBase, IKeysManagementService { + /// + /// Retrieves the public key of a specific user. + /// + /// Request containing the user identifier. + /// The user's public key. public IActionResult getUserKey([FromBody] GetUserKeyRequestDto getUserKeyRequest) { var key = databaseService.GetUserPublicKey(getUserKeyRequest.UserId); @@ -15,6 +20,11 @@ public IActionResult getUserKey([FromBody] GetUserKeyRequestDto getUserKeyReques }); } + /// + /// Gets encryption keys for all groups the requesting user is part of. + /// + /// Request containing the session token. + /// Dictionary of group ids with their encryption keys. public IActionResult getGroupsKey([FromBody] GetGroupsKeyRequestDto getGroupsKeyRequest) { var user = databaseService.GetUserFromToken(getGroupsKeyRequest.SessionToken); diff --git a/src/Cryptie.Server/Features/Messages/Services/MessageHubService.cs b/src/Cryptie.Server/Features/Messages/Services/MessageHubService.cs index e4c48ba..d01fe3c 100644 --- a/src/Cryptie.Server/Features/Messages/Services/MessageHubService.cs +++ b/src/Cryptie.Server/Features/Messages/Services/MessageHubService.cs @@ -12,6 +12,12 @@ public MessageHubService(IHubContext hubContext) _hubContext = hubContext; } + /// + /// Sends a real-time message to all clients in the specified SignalR group. + /// + /// Group identifier. + /// User sending the message. + /// Encrypted message content. public void SendMessageToGroup(Guid group, Guid senderId, string message) { _hubContext.Clients.Group(group.ToString()) diff --git a/src/Cryptie.Server/Features/Messages/Services/MessagesService.cs b/src/Cryptie.Server/Features/Messages/Services/MessagesService.cs index b7001cb..ad703a3 100644 --- a/src/Cryptie.Server/Features/Messages/Services/MessagesService.cs +++ b/src/Cryptie.Server/Features/Messages/Services/MessagesService.cs @@ -7,6 +7,11 @@ namespace Cryptie.Server.Features.Messages.Services; public class MessagesService(IDatabaseService databaseService, IMessageHubService messageHubService) : ControllerBase, IMessagesService { + /// + /// Sends a message to a group on behalf of the authenticated user. + /// + /// Request containing the session token, target group and message. + /// Status of the send operation. public IActionResult SendMessage([FromBody] SendMessageRequestDto sendMessageRequest) { var user = databaseService.GetUserFromToken(sendMessageRequest.SenderToken); @@ -24,6 +29,11 @@ public IActionResult SendMessage([FromBody] SendMessageRequestDto sendMessageReq return Ok(); } + /// + /// Retrieves all messages from a specific group. + /// + /// Request containing the user token and group id. + /// A list of messages in the group. public IActionResult GetGroupMessages([FromBody] GetGroupMessagesRequestDto request) { var user = databaseService.GetUserFromToken(request.UserToken); @@ -49,6 +59,11 @@ public IActionResult GetGroupMessages([FromBody] GetGroupMessagesRequestDto requ return Ok(response); } + /// + /// Retrieves messages from a group posted after the specified timestamp. + /// + /// Request containing group id, token and timestamp. + /// List of messages newer than the provided date. public IActionResult GetGroupMessagesSince([FromBody] GetGroupMessagesSinceRequestDto request) { var user = databaseService.GetUserFromToken(request.UserToken); diff --git a/src/Cryptie.Server/Features/ServerStatus/Services/ServerStatusService.cs b/src/Cryptie.Server/Features/ServerStatus/Services/ServerStatusService.cs index c3173d0..a57da58 100644 --- a/src/Cryptie.Server/Features/ServerStatus/Services/ServerStatusService.cs +++ b/src/Cryptie.Server/Features/ServerStatus/Services/ServerStatusService.cs @@ -4,6 +4,10 @@ namespace Cryptie.Server.Features.ServerStatus.Services; public class ServerStatusService : ControllerBase, IServerStatusService { + /// + /// Simple health check endpoint confirming that the server is running. + /// + /// HTTP 200 response. public IActionResult GetServerStatus() { return Ok(); diff --git a/src/Cryptie.Server/Features/UserManagement/Services/UserManagementService.cs b/src/Cryptie.Server/Features/UserManagement/Services/UserManagementService.cs index ea0615d..6478548 100644 --- a/src/Cryptie.Server/Features/UserManagement/Services/UserManagementService.cs +++ b/src/Cryptie.Server/Features/UserManagement/Services/UserManagementService.cs @@ -6,6 +6,11 @@ namespace Cryptie.Server.Features.UserManagement.Services; public class UserManagementService(IDatabaseService databaseService) : ControllerBase, IUserManagementService { + /// + /// Resolves a user identifier from a valid session token. + /// + /// Request containing the token. + /// User id when the token is valid. public IActionResult UserGuidFromToken([FromBody] UserGuidFromTokenRequestDto userGuidFromTokenRequest) { var user = databaseService.GetUserFromToken(userGuidFromTokenRequest.SessionToken); @@ -17,6 +22,11 @@ public IActionResult UserGuidFromToken([FromBody] UserGuidFromTokenRequestDto us }); } + /// + /// Creates a private group for two users and stores provided encryption keys. + /// + /// Information about the friend and keys. + /// Result of the operation. public IActionResult AddFriend([FromBody] AddFriendRequestDto addFriendRequest) { var friend = databaseService.FindUserByLogin(addFriendRequest.Friend); @@ -44,6 +54,11 @@ public IActionResult AddFriend([FromBody] AddFriendRequestDto addFriendRequest) return Ok(); } + /// + /// Returns the display name of a user identified by GUID. + /// + /// Request containing the user id. + /// User display name. public IActionResult NameFromGuid([FromBody] NameFromGuidRequestDto nameFromGuidRequest) { var user = databaseService.FindUserById(nameFromGuidRequest.Id); @@ -55,6 +70,11 @@ public IActionResult NameFromGuid([FromBody] NameFromGuidRequestDto nameFromGuid }); } + /// + /// Returns identifiers of groups the user belongs to. + /// + /// Request containing the session token. + /// List of group ids. public IActionResult UserGroups([FromBody] UserGroupsRequestDto userGroupsRequest) { var user = databaseService.GetUserFromToken(userGroupsRequest.SessionToken); @@ -68,6 +88,11 @@ public IActionResult UserGroups([FromBody] UserGroupsRequestDto userGroupsReques }); } + /// + /// Changes the display name for the authenticated user. + /// + /// Request containing token and new name. + /// HTTP 200 on success. public IActionResult UserDisplayName([FromBody] UserDisplayNameRequestDto userDisplayNameRequest) { var user = databaseService.GetUserFromToken(userDisplayNameRequest.Token); @@ -78,6 +103,11 @@ public IActionResult UserDisplayName([FromBody] UserDisplayNameRequestDto userDi return Ok(); } + /// + /// Retrieves the private key and control value of a user. + /// + /// Request containing session token. + /// The private key pair. public IActionResult UserPrivateKey([FromBody] UserPrivateKeyRequestDto userPrivateKeyRequest) { var user = databaseService.GetUserFromToken(userPrivateKeyRequest.SessionToken); @@ -90,6 +120,11 @@ public IActionResult UserPrivateKey([FromBody] UserPrivateKeyRequestDto userPriv }); } + /// + /// Finds a user identifier by login. + /// + /// Request containing the login. + /// User id or 404. public IActionResult GuidFromLogin([FromBody] GuidFromLoginRequestDto guidFromLoginRequest) { var user = databaseService.FindUserByLogin(guidFromLoginRequest.Login); diff --git a/src/Cryptie.Server/Services/DatabaseService.cs b/src/Cryptie.Server/Services/DatabaseService.cs index 05a06fe..5c004e4 100644 --- a/src/Cryptie.Server/Services/DatabaseService.cs +++ b/src/Cryptie.Server/Services/DatabaseService.cs @@ -6,6 +6,11 @@ namespace Cryptie.Server.Services; public class DatabaseService(IAppDbContext appDbContext) : IDatabaseService { + /// + /// Retrieves a user associated with the specified session token. + /// + /// Token identifying the user session. + /// The or null when no token is found. public User? GetUserFromToken(Guid guid) { return appDbContext.UserTokens @@ -17,18 +22,33 @@ public class DatabaseService(IAppDbContext appDbContext) : IDatabaseService .User; } + /// + /// Finds a user by their unique identifier. + /// + /// User identifier. + /// The instance or null if not found. public User? FindUserById(Guid id) { var user = appDbContext.Users.Find(id); return user; } + /// + /// Retrieves a user using their login name. + /// + /// Login string. + /// The matching or null. public User? FindUserByLogin(string login) { var user = appDbContext.Users.FirstOrDefault(u => u.Login == login); return user; } + /// + /// Gets a group with its members populated by identifier. + /// + /// Group identifier. + /// The found or null. public Group? FindGroupById(Guid id) { return appDbContext.Groups @@ -36,6 +56,12 @@ public class DatabaseService(IAppDbContext appDbContext) : IDatabaseService .SingleOrDefault(g => g.Id == id); } + /// + /// Creates a new group and assigns the provided user as the first member. + /// + /// User that will own the group. + /// Name of the group. + /// The created entity. public Group CreateNewGroup(User user, string name) { var newGroup = new Group @@ -51,6 +77,11 @@ public Group CreateNewGroup(User user, string name) return createdGroup.Entity; } + /// + /// Adds the specified user to a group. + /// + /// Identifier of the user. + /// Identifier of the group. public void AddUserToGroup(Guid user, Guid group) { var userToAdd = appDbContext.Users.SingleOrDefault(u => u.Id == user); @@ -59,6 +90,11 @@ public void AddUserToGroup(Guid user, Guid group) appDbContext.SaveChanges(); } + /// + /// Removes a user from a group. + /// + /// Identifier of the user. + /// Identifier of the group. public void RemoveUserFromGroup(Guid user, Guid group) { var userToRemove = appDbContext.Users.SingleOrDefault(u => u.Id == user); @@ -67,6 +103,11 @@ public void RemoveUserFromGroup(Guid user, Guid group) appDbContext.SaveChanges(); } + /// + /// Deletes a group with the given identifier. + /// + /// Group identifier. + /// true when group existed and was removed. public bool DeleteGroup(Guid groupGuid) { var group = appDbContext.Groups.SingleOrDefault(g => g.Id == groupGuid); @@ -76,6 +117,12 @@ public bool DeleteGroup(Guid groupGuid) return true; } + /// + /// Changes the name of a group. + /// + /// Group identifier. + /// New group name. + /// true when the group exists and was updated. public bool ChangeGroupName(Guid groupGuid, string name) { var group = appDbContext.Groups.SingleOrDefault(g => g.Id == groupGuid); @@ -87,6 +134,11 @@ public bool ChangeGroupName(Guid groupGuid, string name) return true; } + /// + /// Creates a temporary TOTP token for the specified user. + /// + /// User for whom the token is generated. + /// The identifier of the created token. public Guid CreateTotpToken(User user) { var totpToken = appDbContext.TotpTokens.Add(new TotpToken @@ -101,6 +153,10 @@ public Guid CreateTotpToken(User user) return totpToken.Entity.Id; } + /// + /// Records a login attempt for the given user. + /// + /// User that attempted to log in. public void LogLoginAttempt(User user) { appDbContext.UserLoginAttempts.Add(new UserLoginAttempt @@ -113,6 +169,11 @@ public void LogLoginAttempt(User user) appDbContext.SaveChanges(); } + /// + /// Records a login attempt made with a username that does not correspond to + /// a real user account. + /// + /// The honeypot username. public void LogLoginAttempt(string user) { appDbContext.HoneypotLoginAttempts.Add(new HoneypotLoginAttempt @@ -125,6 +186,11 @@ public void LogLoginAttempt(string user) appDbContext.SaveChanges(); } + /// + /// Generates and persists a new session token for the specified user. + /// + /// User for which the token is generated. + /// Identifier of the created session token. public Guid GenerateUserToken(User user) { var token = appDbContext.UserTokens.Add(new UserToken @@ -138,12 +204,23 @@ public Guid GenerateUserToken(User user) return token.Entity.Id; } + /// + /// Adds another user to the given user's friend list. + /// + /// User who will receive the friend. + /// User to be added. public void AddFriend(User user, User friend) { user.Friends.Add(friend); appDbContext.SaveChanges(); } + /// + /// Creates a group with the provided name. + /// + /// Group name. + /// Indicates whether the group is private. + /// The created entity. public Group CreateGroup(string name, bool isPrivate = false) { var group = appDbContext.Groups.Add(new Group @@ -157,6 +234,11 @@ public Group CreateGroup(string name, bool isPrivate = false) return group.Entity; } + /// + /// Updates the display name of the specified user. + /// + /// User to update. + /// New display name. public void ChangeUserDisplayName(User user, string name) { user.DisplayName = name; @@ -164,6 +246,11 @@ public void ChangeUserDisplayName(User user, string name) appDbContext.SaveChanges(); } + /// + /// Retrieves the public key of a user. + /// + /// User identifier. + /// The public key or an empty string if the user doesn't exist. public string GetUserPublicKey(Guid userId) { var user = appDbContext.Users @@ -173,6 +260,12 @@ public string GetUserPublicKey(Guid userId) return user?.PublicKey ?? string.Empty; } + /// + /// Saves the provided key pair for a user. + /// + /// User to update. + /// Private key value. + /// Public key value. public void SaveUserKeys(User user, string privateKey, string publicKey) { user.PrivateKey = privateKey; @@ -181,6 +274,13 @@ public void SaveUserKeys(User user, string privateKey, string publicKey) appDbContext.SaveChanges(); } + /// + /// Stores a new message in a group and returns the created entity. + /// + /// Destination group. + /// User sending the message. + /// Message content. + /// The stored . public GroupMessage SendGroupMessage(Group group, User sender, string message) { var groupMessage = new GroupMessage @@ -198,6 +298,12 @@ public GroupMessage SendGroupMessage(Group group, User sender, string message) return groupMessage; } + /// + /// Retrieves a specific message from a group. + /// + /// Identifier of the message. + /// Identifier of the group. + /// The if found, otherwise null. public GroupMessage? GetGroupMessage(Guid messageId, Guid groupId) { return appDbContext.GroupMessages @@ -207,6 +313,11 @@ public GroupMessage SendGroupMessage(Group group, User sender, string message) .FirstOrDefault(m => m.Id == messageId && m.GroupId == groupId); } + /// + /// Returns all messages from the specified group ordered by date. + /// + /// Identifier of the group. + /// List of group messages. public List GetGroupMessages(Guid groupId) { return appDbContext.GroupMessages @@ -216,6 +327,12 @@ public List GetGroupMessages(Guid groupId) .ToList(); } + /// + /// Retrieves messages from a group newer than the specified timestamp. + /// + /// Group identifier. + /// Earliest message date to include. + /// List of messages posted after . public List GetGroupMessagesSince(Guid groupId, DateTime since) { return appDbContext.GroupMessages @@ -225,6 +342,12 @@ public List GetGroupMessagesSince(Guid groupId, DateTime since) .ToList(); } + /// + /// Stores an encryption key for a user and group pair. + /// + /// Identifier of the user. + /// Identifier of the group. + /// AES key to store. public void AddGroupEncryptionKey(Guid userId, Guid groupId, string key) { var user = appDbContext.Users @@ -250,6 +373,12 @@ public void AddGroupEncryptionKey(Guid userId, Guid groupId, string key) appDbContext.SaveChanges(); } + /// + /// Retrieves the stored encryption key for a user in a specific group. + /// + /// Identifier of the user. + /// Identifier of the group. + /// The AES key or an empty string when missing. public string getGroupEncryptionKey(Guid userId, Guid groupId) { var key = appDbContext.GroupEncryptionKeys