diff --git a/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs b/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs index 58c622f..c6782aa 100644 --- a/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs +++ b/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs @@ -11,6 +11,7 @@ using Govor.Application.Interfaces.Medias; using Govor.Application.Interfaces.Messages; using Govor.Application.Interfaces.UserOnlineStatus; using Govor.Application.Interfaces.UserSession; +using Govor.Application.Interfaces.UserSession.Crypto; using Govor.Application.Services; using Govor.Application.Services.Authentication; using Govor.Application.Services.Friends; @@ -18,6 +19,7 @@ using Govor.Application.Services.Medias; using Govor.Application.Services.Messages; using Govor.Application.Services.UserOnlineStatus; using Govor.Application.Services.UserSessions; +using Govor.Application.Services.UserSessions.Crypto; using Govor.Core.Infrastructure.Extensions; using Govor.Core.Infrastructure.Validators; using Govor.Core.Models; @@ -91,6 +93,10 @@ public static class ConfigurationProgramExtensions services.AddScoped(); services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } public static void AddRepositories(this IServiceCollection services) diff --git a/Govor.API/Controllers/Authentication/SessionKeysController.cs b/Govor.API/Controllers/Authentication/SessionKeysController.cs index d04fa75..77929ec 100644 --- a/Govor.API/Controllers/Authentication/SessionKeysController.cs +++ b/Govor.API/Controllers/Authentication/SessionKeysController.cs @@ -1,4 +1,7 @@ +using Govor.Application.Interfaces.Friends; using Govor.Application.Interfaces.Infrastructure.Extensions; +using Govor.Application.Interfaces.UserSession; +using Govor.Application.Interfaces.UserSession.Crypto; using Govor.Contracts.Requests; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -12,32 +15,96 @@ namespace Govor.API.Controllers.Authentication; public class SessionKeysController : Controller { private readonly ILogger _logger; + private readonly IFriendshipService _friendshipService; private readonly ICurrentUserSessionService _currentSession; + private readonly ISessionKeyAttacher _sessionKeyAttacher; + private readonly ISessionKeysReader _sessionKeysReader; + private readonly IOneTimePreKeysRotator _oneTimePreKeysRotator; private readonly ICurrentUserService _currentUser; - /* + + public SessionKeysController( + ILogger logger, + IFriendshipService friendshipService, + ICurrentUserSessionService currentSession, + ISessionKeyAttacher sessionKeyAttacher, + ISessionKeysReader sessionKeysReader, + ICurrentUserService currentUser) + { + _logger = logger; + _friendshipService = friendshipService; + _currentSession = currentSession; + _sessionKeyAttacher = sessionKeyAttacher; + _sessionKeysReader = sessionKeysReader; + _currentUser = currentUser; + } + [HttpPost("keys")] public async Task UploadSessionKeys([FromBody] UploadKeysRequest request) { + if (!ModelState.IsValid) + return BadRequest(ModelState); + + if(request.OneTimePreKeys.Count > 100) + return BadRequest("Too many one time pre keys"); + var sessionId = _currentSession.GetUserSessionId(); - await _sessionKeyService.AttachKeysAsync(sessionId, request.PublicEncryptionKey, request.PublicSigningKey); + await _sessionKeyAttacher.AttachKeysAsync(sessionId, + request.IdentityKey, + request.SignedPreKey, + request.SignedPreKeySignature, + request.OneTimePreKeys); return Ok(); } - [Authorize] [HttpGet("users/{userId}/keys")] public async Task GetUserPublicKeys(Guid userId) { - var requesterId = _currentUserService.UserId; + var requesterId = _currentUser.GetCurrentUserId(); - if (!await _friendshipService.AreFriendsAsync(requesterId, userId)) + if (!(await _friendshipService.GetFriendsAsync(userId)).Select(u => u.Id).Contains(requesterId)) return Forbid("You can only access keys of your friends"); - var keys = await _sessionKeyService.GetAllActiveKeysAsync(userId); + var keys = await _sessionKeysReader.GetAllActiveKeysAsync(userId); return Ok(keys); } + + [HttpPost("keys/rotate")] + public async Task RotateOneTimePreKeys([FromBody] RotateOneTimePreKeysRequest request) + { + if (!ModelState.IsValid) + return BadRequest(ModelState); + + if (request.NewOneTimePreKeys.Count > 100) + return BadRequest("Too many new one time pre keys"); + + var sessionId = _currentSession.GetUserSessionId(); + + await _oneTimePreKeysRotator.RotateOneTimePreKeysAsync(sessionId, request.NewOneTimePreKeys); + + return Ok("One-Time PreKeys rotated successfully."); + } + + [HttpGet("keys/remaining")] + public async Task GetRemainingOneTimePreKeysCount() + { + var sessionId = _currentSession.GetUserSessionId(); + + var count = await _sessionKeysReader.GetRemainingOneTimePreKeysCountAsync(sessionId); + + return Ok(new { remaining = count }); + } + + [HttpPost("keys/{preKeyId}/used")] + public async Task MarkOneTimePreKeyAsUsed([FromRoute] Guid preKeyId) + { + var sessionId = _currentSession.GetUserSessionId(); + + await _oneTimePreKeysRotator.MarkOneTimePreKeyAsUsedAsync(sessionId, preKeyId); + + return Ok("Marked as used."); + } - */ } \ No newline at end of file diff --git a/Govor.API/appsettings.json b/Govor.API/appsettings.json index 7bdb15b..ce20070 100644 --- a/Govor.API/appsettings.json +++ b/Govor.API/appsettings.json @@ -16,5 +16,8 @@ }, "JwtRefreshOption": { "RefreshTokenLifetimeDays": 30 + }, + "EncryptionOption": { + "Secret": "4r8B2j9kP5mX7nQwE2zY3A==" } } diff --git a/Govor.Application/Interfaces/UserSession/Crypto/IOneTimePreKeysRotator.cs b/Govor.Application/Interfaces/UserSession/Crypto/IOneTimePreKeysRotator.cs new file mode 100644 index 0000000..9bf0865 --- /dev/null +++ b/Govor.Application/Interfaces/UserSession/Crypto/IOneTimePreKeysRotator.cs @@ -0,0 +1,8 @@ +namespace Govor.Application.Interfaces.UserSession.Crypto; + +public interface IOneTimePreKeysRotator +{ + Task RotateOneTimePreKeysAsync(Guid sessionId, IEnumerable newOneTimePreKeys); + + Task MarkOneTimePreKeyAsUsedAsync(Guid sessionId, Guid oneTimePreKeyId); +} \ No newline at end of file diff --git a/Govor.Application/Interfaces/UserSession/Crypto/ISessionKeyAttacher.cs b/Govor.Application/Interfaces/UserSession/Crypto/ISessionKeyAttacher.cs new file mode 100644 index 0000000..d08db89 --- /dev/null +++ b/Govor.Application/Interfaces/UserSession/Crypto/ISessionKeyAttacher.cs @@ -0,0 +1,13 @@ +using Govor.Core.Models.Users.Crypto; + +namespace Govor.Application.Interfaces.UserSession.Crypto; + +public interface ISessionKeyAttacher +{ + Task AttachKeysAsync( + Guid sessionId, + byte[] identityKey, + byte[] signedPreKey, + byte[] signedPreKeySignature, + IEnumerable oneTimePreKeys); +} \ No newline at end of file diff --git a/Govor.Application/Interfaces/UserSession/Crypto/ISessionKeysReader.cs b/Govor.Application/Interfaces/UserSession/Crypto/ISessionKeysReader.cs new file mode 100644 index 0000000..5a0a23f --- /dev/null +++ b/Govor.Application/Interfaces/UserSession/Crypto/ISessionKeysReader.cs @@ -0,0 +1,11 @@ +using Govor.Core.Models.Users.Crypto; + +namespace Govor.Application.Interfaces.UserSession.Crypto; + +public interface ISessionKeysReader +{ + Task HasKeysAttachedAsync(Guid sessionId); + Task> GetAllActiveKeysAsync(Guid userId); + Task GetRemainingOneTimePreKeysCountAsync(Guid sessionId); + Task GetKeysBySessionIdAsync(Guid sessionId); +} \ No newline at end of file diff --git a/Govor.Application/Interfaces/UserSession/ISessionKeyService.cs b/Govor.Application/Interfaces/UserSession/ISessionKeyService.cs deleted file mode 100644 index 76513b4..0000000 --- a/Govor.Application/Interfaces/UserSession/ISessionKeyService.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Govor.Application.Interfaces.UserSession; - -public interface ISessionKeyService -{ - /// - /// Привязать публичные ключи текущего клиента к сессии. - /// - Task AttachKeysAsync(Guid userId, Guid sessionId, string publicEncryptionKey, string publicSigningKey); - - Task GetKeysAsync(Guid userId, Guid sessionId); - - /// - /// Получить публичные ключи пользователя (например, последнюю активную сессию или все активные). - /// - Task> GetAllActiveKeysAsync(Guid userId); -} - -public class SessionPublicKeys -{ - public Guid UserId { get; set; } - public Guid SessionId { get; set; } - public string PublicEncryptionKey { get; set; } = string.Empty; - public string PublicSigningKey { get; set; } = string.Empty; -} \ No newline at end of file diff --git a/Govor.Application/Services/UserSessions/Crypto/OneTimePreKeysRotator.cs b/Govor.Application/Services/UserSessions/Crypto/OneTimePreKeysRotator.cs new file mode 100644 index 0000000..0e5856c --- /dev/null +++ b/Govor.Application/Services/UserSessions/Crypto/OneTimePreKeysRotator.cs @@ -0,0 +1,70 @@ +using Govor.Application.Interfaces.Infrastructure.Extensions; +using Govor.Application.Interfaces.UserSession.Crypto; +using Govor.Core.Models.Users.Crypto; +using Govor.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Govor.Application.Services.UserSessions.Crypto; + +public class OneTimePreKeysRotator : IOneTimePreKeysRotator +{ + private readonly ILogger _logger; + private readonly ICurrentUserService _current; + private readonly GovorDbContext _context; + + public OneTimePreKeysRotator(ILogger logger, ICurrentUserService current, GovorDbContext context) + { + _logger = logger; + _current = current; + _context = context; + } + + public async Task RotateOneTimePreKeysAsync(Guid sessionId, IEnumerable newOneTimePreKeys) + { + var cryptoSession = await _context.UserCryptoSessions + .Include(c => c.OneTimePreKeys) + .FirstOrDefaultAsync(c => c.UserSessionId == sessionId); + + if (cryptoSession == null) + throw new ArgumentException("Crypto session not found", nameof(sessionId)); + + // Удаляем все использованные ключи + var usedKeys = cryptoSession.OneTimePreKeys.Where(k => k.IsUsed).ToList(); + if (usedKeys.Any()) + { + _context.OneTimePreKeys.RemoveRange(usedKeys); + } + + // Добавляем новые ключи (возможно, стоит ограничить количество) + foreach (var key in newOneTimePreKeys) + { + cryptoSession.OneTimePreKeys.Add(new OneTimePreKey + { + Id = Guid.NewGuid(), + PublicKey = key, + IsUsed = false, + UploadedAt = DateTime.UtcNow + }); + } + + await _context.SaveChangesAsync(); + } + + public async Task MarkOneTimePreKeyAsUsedAsync(Guid sessionId, Guid oneTimePreKeyId) + { + var key = await _context.OneTimePreKeys + .Include(k => k.UserCryptoSession) + .FirstOrDefaultAsync(k => k.Id == oneTimePreKeyId && k.UserCryptoSession.UserSessionId == sessionId); + + if (key == null) + throw new ArgumentException("One-Time PreKey not found for this session", nameof(oneTimePreKeyId)); + + if (key.IsUsed) + return; // Уже помечен + + key.IsUsed = true; + + await _context.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/Govor.Application/Services/UserSessions/Crypto/SessionKeyAttacher.cs b/Govor.Application/Services/UserSessions/Crypto/SessionKeyAttacher.cs new file mode 100644 index 0000000..1a42da3 --- /dev/null +++ b/Govor.Application/Services/UserSessions/Crypto/SessionKeyAttacher.cs @@ -0,0 +1,73 @@ +using Govor.Application.Interfaces.Infrastructure.Extensions; +using Govor.Application.Interfaces.UserSession.Crypto; +using Govor.Core.Models.Users.Crypto; +using Govor.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Govor.Application.Services.UserSessions.Crypto; + +public class SessionKeyAttacher : ISessionKeyAttacher +{ + private readonly ILogger _logger; + private readonly ICurrentUserService _current; + private readonly GovorDbContext _context; + + public SessionKeyAttacher(ILogger logger, ICurrentUserService current, GovorDbContext context) + { + _logger = logger; + _current = current; + _context = context; + } + + public async Task AttachKeysAsync( + Guid sessionId, + byte[] identityKey, + byte[] signedPreKey, + byte[] signedPreKeySignature, + IEnumerable oneTimePreKeys) + { + var session = await _context.UserSessions + .Include(s => s.CryptoSession) + .ThenInclude(c => c.OneTimePreKeys) + .FirstOrDefaultAsync(s => s.Id == sessionId); + + if (session == null) + throw new ArgumentException("Session not found", nameof(sessionId)); + + if (session.CryptoSession != null) + throw new InvalidOperationException("Keys are already attached to this session"); + + if(session.UserId != _current.GetCurrentUserId()) + throw new InvalidOperationException("You cannot attach keys to this session"); + + var cryptoSessionId = Guid.NewGuid(); + + var cryptoSession = new UserCryptoSession + { + Id = cryptoSessionId, + UserSessionId = sessionId, + PublicIdentityKey = identityKey, + SignedPreKey = new SignedPreKey() + { + Id = Guid.NewGuid(), + UserCryptoSessionId = cryptoSessionId, + PublicSignedPreKey = signedPreKey, + SignedPreKeySignature = signedPreKeySignature, + }, + OneTimePreKeys = oneTimePreKeys.Select(key => new OneTimePreKey + { + Id = Guid.NewGuid(), + PublicKey = key, + IsUsed = false, + UploadedAt = DateTime.UtcNow + }).ToList() + }; + + session.CryptoSession = cryptoSession; + + await _context.SaveChangesAsync(); + + _logger.LogInformation("Attached keys to session {SessionId}", sessionId); + } +} \ No newline at end of file diff --git a/Govor.Application/Services/UserSessions/Crypto/SessionKeysReader.cs b/Govor.Application/Services/UserSessions/Crypto/SessionKeysReader.cs new file mode 100644 index 0000000..8a9e81a --- /dev/null +++ b/Govor.Application/Services/UserSessions/Crypto/SessionKeysReader.cs @@ -0,0 +1,62 @@ +using Govor.Application.Interfaces.Infrastructure.Extensions; +using Govor.Application.Interfaces.UserSession.Crypto; +using Govor.Core.Models.Users.Crypto; +using Govor.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Govor.Application.Services.UserSessions.Crypto; + +public class SessionKeysReader : ISessionKeysReader +{ + private readonly ILogger _logger; + private readonly GovorDbContext _context; + + public SessionKeysReader(ILogger logger, GovorDbContext context) + { + _logger = logger; + _context = context; + } + + public async Task HasKeysAttachedAsync(Guid sessionId) + { + return await _context.UserCryptoSessions.AnyAsync(c => c.UserSessionId == sessionId); + } + + public async Task> GetAllActiveKeysAsync(Guid userId) + { + _logger.LogInformation("Getting all active keys for user {UserId}.", userId); + + var now = DateTime.UtcNow; + + return await _context.UserCryptoSessions + .AsNoTracking() + .Include(c => c.OneTimePreKeys) + .Include(c => c.UserSession) + .Where(c => c.UserSession.UserId == userId + && !c.UserSession.IsRevoked + && c.UserSession.ExpiresAt > now) + .ToListAsync(); + } + + public async Task GetRemainingOneTimePreKeysCountAsync(Guid sessionId) + { + _logger.LogInformation("Getting count of one time pre keys for session {UserId}.", sessionId); + + return await _context.OneTimePreKeys + .AsNoTracking() + .Include(f => f.UserCryptoSession) + .CountAsync(f => f.UserCryptoSession.UserSessionId == sessionId); + } + + public async Task GetKeysBySessionIdAsync(Guid sessionId) + { + _logger.LogInformation("Getting keys for session {session}.", sessionId); + + return await _context.UserCryptoSessions + .AsNoTracking() + .Include(c => c.OneTimePreKeys) + .Include(c => c.UserSession) + .FirstOrDefaultAsync(c => c.UserSessionId == sessionId); + } +} \ No newline at end of file diff --git a/Govor.Contracts/DTOs/OneTimePreKeyDto.cs b/Govor.Contracts/DTOs/OneTimePreKeyDto.cs new file mode 100644 index 0000000..5c2d087 --- /dev/null +++ b/Govor.Contracts/DTOs/OneTimePreKeyDto.cs @@ -0,0 +1,7 @@ +namespace Govor.Contracts.DTOs; + +public class OneTimePreKeyDto +{ + public Guid Id { get; set; } + public string Key { get; set; } = string.Empty; +} diff --git a/Govor.Contracts/DTOs/PublicSessionKeysDto.cs b/Govor.Contracts/DTOs/PublicSessionKeysDto.cs new file mode 100644 index 0000000..96a14a8 --- /dev/null +++ b/Govor.Contracts/DTOs/PublicSessionKeysDto.cs @@ -0,0 +1,8 @@ +namespace Govor.Contracts.DTOs; + +public class PublicSessionKeysDto +{ + public string IdentityKey { get; set; } = string.Empty; // base64 + public SignedPreKeyDto SignedPreKey { get; set; } + public List OneTimePreKeys { get; set; } = new(); +} diff --git a/Govor.Contracts/DTOs/SignedPreKeyDto.cs b/Govor.Contracts/DTOs/SignedPreKeyDto.cs new file mode 100644 index 0000000..798acd5 --- /dev/null +++ b/Govor.Contracts/DTOs/SignedPreKeyDto.cs @@ -0,0 +1,8 @@ +namespace Govor.Contracts.DTOs; + +public class SignedPreKeyDto +{ + public Guid Id { get; set; } + public string Key { get; set; } = string.Empty; + public string Signature { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Govor.Contracts/Requests/RotateOneTimePreKeysRequest.cs b/Govor.Contracts/Requests/RotateOneTimePreKeysRequest.cs new file mode 100644 index 0000000..6553573 --- /dev/null +++ b/Govor.Contracts/Requests/RotateOneTimePreKeysRequest.cs @@ -0,0 +1,6 @@ +namespace Govor.Contracts.Requests; + +public class RotateOneTimePreKeysRequest +{ + public ICollection NewOneTimePreKeys { get; set; } = new List(); +} diff --git a/Govor.Contracts/Requests/UploadKeysRequest.cs b/Govor.Contracts/Requests/UploadKeysRequest.cs index c62cc78..f8d48ff 100644 --- a/Govor.Contracts/Requests/UploadKeysRequest.cs +++ b/Govor.Contracts/Requests/UploadKeysRequest.cs @@ -2,6 +2,8 @@ namespace Govor.Contracts.Requests; public class UploadKeysRequest { - public string PublicEncryptionKey { get; set; } - public string PublicSigningKey { get; set; } + public byte[] IdentityKey { get; set; } + public byte[] SignedPreKey { get; set; } + public byte[] SignedPreKeySignature { get; set; } + public List OneTimePreKeys { get; set; } = new(); } \ No newline at end of file diff --git a/Govor.Core/Models/Users/Crypto/OneTimePreKey.cs b/Govor.Core/Models/Users/Crypto/OneTimePreKey.cs new file mode 100644 index 0000000..b356bb3 --- /dev/null +++ b/Govor.Core/Models/Users/Crypto/OneTimePreKey.cs @@ -0,0 +1,12 @@ +namespace Govor.Core.Models.Users.Crypto; + +public class OneTimePreKey +{ + public Guid Id { get; set; } + public Guid UserCryptoSessionId { get; set; } + public UserCryptoSession UserCryptoSession { get; set; } + + public byte[] PublicKey { get; set; } + public bool IsUsed { get; set; } + public DateTime UploadedAt { get; set; } = DateTime.UtcNow; +} diff --git a/Govor.Core/Models/Users/Crypto/SignedPreKey.cs b/Govor.Core/Models/Users/Crypto/SignedPreKey.cs new file mode 100644 index 0000000..c1b8b27 --- /dev/null +++ b/Govor.Core/Models/Users/Crypto/SignedPreKey.cs @@ -0,0 +1,10 @@ +namespace Govor.Core.Models.Users.Crypto; + +public class SignedPreKey +{ + public Guid Id { get; set; } + public Guid UserCryptoSessionId { get; set; } + public UserCryptoSession UserCryptoSession { get; set; } + public byte[] PublicSignedPreKey { get; set; } + public byte[] SignedPreKeySignature { get; set; } +} \ No newline at end of file diff --git a/Govor.Core/Models/Users/Crypto/UserCryptoSession.cs b/Govor.Core/Models/Users/Crypto/UserCryptoSession.cs new file mode 100644 index 0000000..41403e6 --- /dev/null +++ b/Govor.Core/Models/Users/Crypto/UserCryptoSession.cs @@ -0,0 +1,14 @@ +namespace Govor.Core.Models.Users.Crypto; + +public class UserCryptoSession +{ + public Guid Id { get; set; } + + public Guid UserSessionId { get; set; } + public UserSession UserSession { get; set; } + + public byte[] PublicIdentityKey { get; set; } + + public SignedPreKey SignedPreKey { get; set; } + public ICollection OneTimePreKeys { get; set; } +} diff --git a/Govor.Core/Models/Users/UserSession.cs b/Govor.Core/Models/Users/UserSession.cs index b9cf558..da4728e 100644 --- a/Govor.Core/Models/Users/UserSession.cs +++ b/Govor.Core/Models/Users/UserSession.cs @@ -1,3 +1,5 @@ +using Govor.Core.Models.Users.Crypto; + namespace Govor.Core.Models.Users; public class UserSession @@ -6,11 +8,11 @@ public class UserSession public Guid UserId { get; set; } public string RefreshToken { get; set; } = string.Empty; public string DeviceInfo { get; set; } = string.Empty; // "Chrome on Windows" - public string PublicEncryptionKey { get; set; } - public string PublicSigningKey { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime ExpiresAt { get; set; } public bool IsRevoked { get; set; } = false; + + public UserCryptoSession CryptoSession { get; set; } public override bool Equals(object? obj) { diff --git a/Govor.Data/Configurations/OneTimePreKeyConfiguration.cs b/Govor.Data/Configurations/OneTimePreKeyConfiguration.cs new file mode 100644 index 0000000..0b3c347 --- /dev/null +++ b/Govor.Data/Configurations/OneTimePreKeyConfiguration.cs @@ -0,0 +1,33 @@ +using Govor.Core.Models.Users.Crypto; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Govor.Data.Configurations; + +public class OneTimePreKeyConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + // Первичный ключ + builder.HasKey(otpk => otpk.Id); + + builder.HasOne(otpk => otpk.UserCryptoSession) + .WithMany(ucs => ucs.OneTimePreKeys) + .HasForeignKey(otpk => otpk.UserCryptoSessionId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + + builder.Property(otpk => otpk.PublicKey) + .IsRequired(); + + builder.Property(otpk => otpk.IsUsed) + .IsRequired() + .HasDefaultValue(false); + + builder.Property(otpk => otpk.UploadedAt) + .IsRequired(); + + builder.HasIndex(otpk => new { otpk.UserCryptoSessionId, otpk.IsUsed }); + builder.HasIndex(otpk => otpk.UploadedAt); + } +} \ No newline at end of file diff --git a/Govor.Data/Configurations/SignedPreKeyConfiguration.cs b/Govor.Data/Configurations/SignedPreKeyConfiguration.cs new file mode 100644 index 0000000..f28e1c4 --- /dev/null +++ b/Govor.Data/Configurations/SignedPreKeyConfiguration.cs @@ -0,0 +1,27 @@ +using Govor.Core.Models.Users.Crypto; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Govor.Data.Configurations; + +public class SignedPreKeyConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(spk => spk.Id); + + builder.HasOne(spk => spk.UserCryptoSession) + .WithOne(ucs => ucs.SignedPreKey) + .HasForeignKey(spk => spk.UserCryptoSessionId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + + builder.Property(spk => spk.PublicSignedPreKey) + .IsRequired(); + builder.Property(spk => spk.SignedPreKeySignature) + .IsRequired(); + + builder.HasIndex(spk => spk.UserCryptoSessionId) + .IsUnique(); + } +} \ No newline at end of file diff --git a/Govor.Data/Configurations/UserCryptoSessionConfiguration.cs b/Govor.Data/Configurations/UserCryptoSessionConfiguration.cs new file mode 100644 index 0000000..73a8d73 --- /dev/null +++ b/Govor.Data/Configurations/UserCryptoSessionConfiguration.cs @@ -0,0 +1,37 @@ +using Govor.Core.Models.Users.Crypto; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Govor.Data.Configurations; + +public class UserCryptoSessionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(ucs => ucs.Id); + + builder.HasOne(ucs => ucs.UserSession) + .WithOne(us => us.CryptoSession) + .HasForeignKey(ucs => ucs.UserSessionId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(ucs => ucs.SignedPreKey) + .WithOne(spk => spk.UserCryptoSession) + .HasForeignKey(spk => spk.UserCryptoSessionId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(ucs => ucs.OneTimePreKeys) + .WithOne(otpk => otpk.UserCryptoSession) + .HasForeignKey(otpk => otpk.UserCryptoSessionId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasIndex(ucs => ucs.UserSessionId) + .IsUnique(); + + builder.Property(ucs => ucs.PublicIdentityKey) + .IsRequired(); + } +} \ No newline at end of file diff --git a/Govor.Data/Configurations/UserSessionConfiguration.cs b/Govor.Data/Configurations/UserSessionConfiguration.cs index 0c0c297..d81b485 100644 --- a/Govor.Data/Configurations/UserSessionConfiguration.cs +++ b/Govor.Data/Configurations/UserSessionConfiguration.cs @@ -1,5 +1,6 @@ using Govor.Core.Models; using Govor.Core.Models.Users; +using Govor.Core.Models.Users.Crypto; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -15,11 +16,16 @@ public class UserSessionConfiguration : IEntityTypeConfiguration .IsRequired(); builder.Property(us => us.DeviceInfo) - .HasMaxLength(200); + .HasMaxLength(256); builder.HasOne() .WithMany() .HasForeignKey(us => us.UserId) .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(e => e.CryptoSession) + .WithOne(e => e.UserSession) + .HasForeignKey(e => e.UserSessionId) + .OnDelete(DeleteBehavior.Cascade); } } \ No newline at end of file diff --git a/Govor.Data/GovorDbContext.cs b/Govor.Data/GovorDbContext.cs index 47ce2fc..1593fe2 100644 --- a/Govor.Data/GovorDbContext.cs +++ b/Govor.Data/GovorDbContext.cs @@ -1,6 +1,7 @@ using Govor.Core.Models; using Govor.Core.Models.Messages; using Govor.Core.Models.Users; +using Govor.Core.Models.Users.Crypto; using Govor.Data.Configurations; using Microsoft.EntityFrameworkCore; @@ -10,6 +11,9 @@ public class GovorDbContext(DbContextOptions options) : DbContex { public virtual DbSet Users { get; set; } public virtual DbSet UserSessions { get; set; } + public virtual DbSet UserCryptoSessions { get; set; } + public virtual DbSet SignedPreKeys { get; set; } + public virtual DbSet OneTimePreKeys { get; set; } public virtual DbSet Friendships { get; set; } public virtual DbSet PrivateChats { get; set; } public virtual DbSet Admins { get; set; } @@ -43,7 +47,10 @@ public class GovorDbContext(DbContextOptions options) : DbContex modelBuilder.ApplyConfiguration(new GroupInvitationConfiguration()); modelBuilder.ApplyConfiguration(new GroupMembershipConfiguration()); modelBuilder.ApplyConfiguration(new GroupAdminsConfiguration()); - + + modelBuilder.ApplyConfiguration(new OneTimePreKeyConfiguration()); + modelBuilder.ApplyConfiguration(new UserCryptoSessionConfiguration()); + modelBuilder.ApplyConfiguration(new SignedPreKeyConfiguration()); base.OnModelCreating(modelBuilder); } } \ No newline at end of file diff --git a/Govor.Data/Migrations/20250730142221_CryptKeys.Designer.cs b/Govor.Data/Migrations/20250730142221_CryptKeys.Designer.cs new file mode 100644 index 0000000..e2a9184 --- /dev/null +++ b/Govor.Data/Migrations/20250730142221_CryptKeys.Designer.cs @@ -0,0 +1,763 @@ +// +using System; +using Govor.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Govor.Data.Migrations +{ + [DbContext(typeof(GovorDbContext))] + [Migration("20250730142221_CryptKeys")] + partial class CryptKeys + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Govor.Core.Models.ChatGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ImageId") + .HasColumnType("char(36)"); + + b.Property("IsChannel") + .HasColumnType("tinyint(1)"); + + b.Property("IsPrivate") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.ToTable("ChatGroups"); + }); + + modelBuilder.Entity("Govor.Core.Models.Friendship", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AddresseeId") + .HasColumnType("char(36)"); + + b.Property("RequesterId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AddresseeId"); + + b.HasIndex("RequesterId"); + + b.ToTable("Friendships"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupAdmins", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("GroupAdmins"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupInvitation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InvitationCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("MaxParticipants") + .HasColumnType("int"); + + b.Property("UserMakerId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("UserMakerId"); + + b.ToTable("GroupInvitations"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InvitationId") + .HasColumnType("char(36)"); + + b.Property("IsBanned") + .HasColumnType("tinyint(1)"); + + b.Property("MemberSince") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("InvitationId"); + + b.ToTable("GroupMemberships"); + }); + + modelBuilder.Entity("Govor.Core.Models.Invitation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsAdmin") + .HasColumnType("tinyint(1)"); + + b.Property("MaxParticipants") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Invitations"); + }); + + modelBuilder.Entity("Govor.Core.Models.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)"); + + b.Property("MediaType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MineType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UploaderId") + .HasColumnType("char(36)"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("MediaFiles"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MediaAttachments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("MediaFileId") + .HasColumnType("char(36)"); + + b.Property("MessageId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("MediaFileId"); + + b.HasIndex("MessageId"); + + b.ToTable("MediaAttachments"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ChatGroupId") + .HasColumnType("char(36)"); + + b.Property("EditedAt") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsEdited") + .HasColumnType("tinyint(1)"); + + b.Property("PrivateChatId") + .HasColumnType("char(36)"); + + b.Property("RecipientId") + .HasColumnType("char(36)"); + + b.Property("RecipientType") + .HasColumnType("int"); + + b.Property("ReplyToMessageId") + .HasColumnType("char(36)"); + + b.Property("SenderId") + .HasColumnType("char(36)"); + + b.Property("SentAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ChatGroupId"); + + b.HasIndex("PrivateChatId"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MessageReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("MessageId") + .HasColumnType("char(36)"); + + b.Property("ReactedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReactionCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("MessageId", "UserId") + .IsUnique(); + + b.ToTable("MessageReactions"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MessageView", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("MessageId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("ViewedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId") + .IsUnique(); + + b.ToTable("MessageViews"); + }); + + modelBuilder.Entity("Govor.Core.Models.PrivateChat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("UserAId") + .HasColumnType("char(36)"); + + b.Property("UserBId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("PrivateChats"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Admin", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("UserId"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.OneTimePreKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("UploadedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserCryptoSessionId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UploadedAt"); + + b.HasIndex("UserCryptoSessionId", "IsUsed"); + + b.ToTable("OneTimePreKeys"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.SignedPreKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("PublicSignedPreKey") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("SignedPreKeySignature") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("UserCryptoSessionId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserCryptoSessionId") + .IsUnique(); + + b.ToTable("SignedPreKeys"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.UserCryptoSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("PublicIdentityKey") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("UserSessionId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserSessionId") + .IsUnique(); + + b.ToTable("UserCryptoSessions"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedOn") + .HasColumnType("date"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IconId") + .HasColumnType("char(36)"); + + b.Property("InviteId") + .HasColumnType("char(36)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Username") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WasOnline") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("InviteId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeviceInfo") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("IsRevoked") + .HasColumnType("tinyint(1)"); + + b.Property("RefreshToken") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Govor.Core.Models.Friendship", b => + { + b.HasOne("Govor.Core.Models.Users.User", "Addressee") + .WithMany("ReceivedFriendRequests") + .HasForeignKey("AddresseeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Govor.Core.Models.Users.User", "Requester") + .WithMany("SentFriendRequests") + .HasForeignKey("RequesterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Addressee"); + + b.Navigation("Requester"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupAdmins", b => + { + b.HasOne("Govor.Core.Models.ChatGroup", null) + .WithMany("Admins") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupInvitation", b => + { + b.HasOne("Govor.Core.Models.ChatGroup", null) + .WithMany("InviteCodes") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Govor.Core.Models.Users.User", "UserMaker") + .WithMany() + .HasForeignKey("UserMakerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("UserMaker"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupMembership", b => + { + b.HasOne("Govor.Core.Models.ChatGroup", null) + .WithMany("Members") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Govor.Core.Models.GroupInvitation", null) + .WithMany() + .HasForeignKey("InvitationId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MediaAttachments", b => + { + b.HasOne("Govor.Core.Models.MediaFile", "MediaFile") + .WithMany() + .HasForeignKey("MediaFileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Govor.Core.Models.Messages.Message", "Message") + .WithMany("MediaAttachments") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaFile"); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.Message", b => + { + b.HasOne("Govor.Core.Models.ChatGroup", null) + .WithMany("Messages") + .HasForeignKey("ChatGroupId"); + + b.HasOne("Govor.Core.Models.PrivateChat", null) + .WithMany("Messages") + .HasForeignKey("PrivateChatId"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MessageReaction", b => + { + b.HasOne("Govor.Core.Models.Messages.Message", "Message") + .WithMany("Reactions") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Govor.Core.Models.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MessageView", b => + { + b.HasOne("Govor.Core.Models.Messages.Message", null) + .WithMany("MessageViews") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Admin", b => + { + b.HasOne("Govor.Core.Models.Users.User", "User") + .WithOne() + .HasForeignKey("Govor.Core.Models.Users.Admin", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.OneTimePreKey", b => + { + b.HasOne("Govor.Core.Models.Users.Crypto.UserCryptoSession", "UserCryptoSession") + .WithMany("OneTimePreKeys") + .HasForeignKey("UserCryptoSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserCryptoSession"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.SignedPreKey", b => + { + b.HasOne("Govor.Core.Models.Users.Crypto.UserCryptoSession", "UserCryptoSession") + .WithOne("SignedPreKey") + .HasForeignKey("Govor.Core.Models.Users.Crypto.SignedPreKey", "UserCryptoSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserCryptoSession"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.UserCryptoSession", b => + { + b.HasOne("Govor.Core.Models.Users.UserSession", "UserSession") + .WithOne("CryptoSession") + .HasForeignKey("Govor.Core.Models.Users.Crypto.UserCryptoSession", "UserSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserSession"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.User", b => + { + b.HasOne("Govor.Core.Models.Invitation", "Invite") + .WithMany("Users") + .HasForeignKey("InviteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Invite"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.UserSession", b => + { + b.HasOne("Govor.Core.Models.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Govor.Core.Models.ChatGroup", b => + { + b.Navigation("Admins"); + + b.Navigation("InviteCodes"); + + b.Navigation("Members"); + + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Govor.Core.Models.Invitation", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.Message", b => + { + b.Navigation("MediaAttachments"); + + b.Navigation("MessageViews"); + + b.Navigation("Reactions"); + }); + + modelBuilder.Entity("Govor.Core.Models.PrivateChat", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.UserCryptoSession", b => + { + b.Navigation("OneTimePreKeys"); + + b.Navigation("SignedPreKey") + .IsRequired(); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.User", b => + { + b.Navigation("ReceivedFriendRequests"); + + b.Navigation("SentFriendRequests"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.UserSession", b => + { + b.Navigation("CryptoSession") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Govor.Data/Migrations/20250730142221_CryptKeys.cs b/Govor.Data/Migrations/20250730142221_CryptKeys.cs new file mode 100644 index 0000000..035402f --- /dev/null +++ b/Govor.Data/Migrations/20250730142221_CryptKeys.cs @@ -0,0 +1,170 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Govor.Data.Migrations +{ + /// + public partial class CryptKeys : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "DeviceInfo", + table: "UserSessions", + type: "varchar(256)", + maxLength: 256, + nullable: false, + oldClrType: typeof(string), + oldType: "varchar(200)", + oldMaxLength: 200) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "InvitationId", + table: "GroupMemberships", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci", + oldClrType: typeof(Guid), + oldType: "char(36)") + .OldAnnotation("Relational:Collation", "ascii_general_ci"); + + migrationBuilder.AddColumn( + name: "MemberSince", + table: "GroupMemberships", + type: "datetime(6)", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.CreateTable( + name: "UserCryptoSessions", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + UserSessionId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + PublicIdentityKey = table.Column(type: "longblob", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserCryptoSessions", x => x.Id); + table.ForeignKey( + name: "FK_UserCryptoSessions_UserSessions_UserSessionId", + column: x => x.UserSessionId, + principalTable: "UserSessions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "OneTimePreKeys", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + UserCryptoSessionId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + PublicKey = table.Column(type: "longblob", nullable: false), + IsUsed = table.Column(type: "tinyint(1)", nullable: false, defaultValue: false), + UploadedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OneTimePreKeys", x => x.Id); + table.ForeignKey( + name: "FK_OneTimePreKeys_UserCryptoSessions_UserCryptoSessionId", + column: x => x.UserCryptoSessionId, + principalTable: "UserCryptoSessions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "SignedPreKeys", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + UserCryptoSessionId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + PublicSignedPreKey = table.Column(type: "longblob", nullable: false), + SignedPreKeySignature = table.Column(type: "longblob", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SignedPreKeys", x => x.Id); + table.ForeignKey( + name: "FK_SignedPreKeys_UserCryptoSessions_UserCryptoSessionId", + column: x => x.UserCryptoSessionId, + principalTable: "UserCryptoSessions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_OneTimePreKeys_UploadedAt", + table: "OneTimePreKeys", + column: "UploadedAt"); + + migrationBuilder.CreateIndex( + name: "IX_OneTimePreKeys_UserCryptoSessionId_IsUsed", + table: "OneTimePreKeys", + columns: new[] { "UserCryptoSessionId", "IsUsed" }); + + migrationBuilder.CreateIndex( + name: "IX_SignedPreKeys_UserCryptoSessionId", + table: "SignedPreKeys", + column: "UserCryptoSessionId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UserCryptoSessions_UserSessionId", + table: "UserCryptoSessions", + column: "UserSessionId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OneTimePreKeys"); + + migrationBuilder.DropTable( + name: "SignedPreKeys"); + + migrationBuilder.DropTable( + name: "UserCryptoSessions"); + + migrationBuilder.DropColumn( + name: "MemberSince", + table: "GroupMemberships"); + + migrationBuilder.AlterColumn( + name: "DeviceInfo", + table: "UserSessions", + type: "varchar(200)", + maxLength: 200, + nullable: false, + oldClrType: typeof(string), + oldType: "varchar(256)", + oldMaxLength: 256) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "InvitationId", + table: "GroupMemberships", + type: "char(36)", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + collation: "ascii_general_ci", + oldClrType: typeof(Guid), + oldType: "char(36)", + oldNullable: true) + .OldAnnotation("Relational:Collation", "ascii_general_ci"); + } + } +} diff --git a/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs b/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs index 49f1d34..084ac34 100644 --- a/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs +++ b/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs @@ -145,12 +145,14 @@ namespace Govor.Data.Migrations .HasColumnType("char(36)"); b.Property("InvitationId") - .IsRequired() .HasColumnType("char(36)"); b.Property("IsBanned") .HasColumnType("tinyint(1)"); + b.Property("MemberSince") + .HasColumnType("datetime(6)"); + b.Property("UserId") .HasColumnType("char(36)"); @@ -364,40 +366,6 @@ namespace Govor.Data.Migrations b.ToTable("PrivateChats"); }); - modelBuilder.Entity("Govor.Core.Models.UserSession", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("char(36)"); - - b.Property("CreatedAt") - .HasColumnType("datetime(6)"); - - b.Property("DeviceInfo") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("varchar(200)"); - - b.Property("ExpiresAt") - .HasColumnType("datetime(6)"); - - b.Property("IsRevoked") - .HasColumnType("tinyint(1)"); - - b.Property("RefreshToken") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("UserId") - .HasColumnType("char(36)"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("UserSessions"); - }); - modelBuilder.Entity("Govor.Core.Models.Users.Admin", b => { b.Property("UserId") @@ -408,6 +376,82 @@ namespace Govor.Data.Migrations b.ToTable("Admins"); }); + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.OneTimePreKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("UploadedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserCryptoSessionId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UploadedAt"); + + b.HasIndex("UserCryptoSessionId", "IsUsed"); + + b.ToTable("OneTimePreKeys"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.SignedPreKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("PublicSignedPreKey") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("SignedPreKeySignature") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("UserCryptoSessionId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserCryptoSessionId") + .IsUnique(); + + b.ToTable("SignedPreKeys"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.UserCryptoSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("PublicIdentityKey") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("UserSessionId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserSessionId") + .IsUnique(); + + b.ToTable("UserCryptoSessions"); + }); + modelBuilder.Entity("Govor.Core.Models.Users.User", b => { b.Property("Id") @@ -445,6 +489,40 @@ namespace Govor.Data.Migrations b.ToTable("Users"); }); + modelBuilder.Entity("Govor.Core.Models.Users.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeviceInfo") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("IsRevoked") + .HasColumnType("tinyint(1)"); + + b.Property("RefreshToken") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserSessions"); + }); + modelBuilder.Entity("Govor.Core.Models.Friendship", b => { b.HasOne("Govor.Core.Models.Users.User", "Addressee") @@ -501,8 +579,7 @@ namespace Govor.Data.Migrations b.HasOne("Govor.Core.Models.GroupInvitation", null) .WithMany() .HasForeignKey("InvitationId") - .OnDelete(DeleteBehavior.SetNull) - .IsRequired(); + .OnDelete(DeleteBehavior.SetNull); }); modelBuilder.Entity("Govor.Core.Models.Messages.MediaAttachments", b => @@ -563,15 +640,6 @@ namespace Govor.Data.Migrations .IsRequired(); }); - modelBuilder.Entity("Govor.Core.Models.UserSession", b => - { - b.HasOne("Govor.Core.Models.Users.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("Govor.Core.Models.Users.Admin", b => { b.HasOne("Govor.Core.Models.Users.User", "User") @@ -583,6 +651,39 @@ namespace Govor.Data.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.OneTimePreKey", b => + { + b.HasOne("Govor.Core.Models.Users.Crypto.UserCryptoSession", "UserCryptoSession") + .WithMany("OneTimePreKeys") + .HasForeignKey("UserCryptoSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserCryptoSession"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.SignedPreKey", b => + { + b.HasOne("Govor.Core.Models.Users.Crypto.UserCryptoSession", "UserCryptoSession") + .WithOne("SignedPreKey") + .HasForeignKey("Govor.Core.Models.Users.Crypto.SignedPreKey", "UserCryptoSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserCryptoSession"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.UserCryptoSession", b => + { + b.HasOne("Govor.Core.Models.Users.UserSession", "UserSession") + .WithOne("CryptoSession") + .HasForeignKey("Govor.Core.Models.Users.Crypto.UserCryptoSession", "UserSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserSession"); + }); + modelBuilder.Entity("Govor.Core.Models.Users.User", b => { b.HasOne("Govor.Core.Models.Invitation", "Invite") @@ -594,6 +695,15 @@ namespace Govor.Data.Migrations b.Navigation("Invite"); }); + modelBuilder.Entity("Govor.Core.Models.Users.UserSession", b => + { + b.HasOne("Govor.Core.Models.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Govor.Core.Models.ChatGroup", b => { b.Navigation("Admins"); @@ -624,12 +734,26 @@ namespace Govor.Data.Migrations b.Navigation("Messages"); }); + modelBuilder.Entity("Govor.Core.Models.Users.Crypto.UserCryptoSession", b => + { + b.Navigation("OneTimePreKeys"); + + b.Navigation("SignedPreKey") + .IsRequired(); + }); + modelBuilder.Entity("Govor.Core.Models.Users.User", b => { b.Navigation("ReceivedFriendRequests"); b.Navigation("SentFriendRequests"); }); + + modelBuilder.Entity("Govor.Core.Models.Users.UserSession", b => + { + b.Navigation("CryptoSession") + .IsRequired(); + }); #pragma warning restore 612, 618 } }