diff --git a/src/Core/KeyManagement/Models/Data/UserAsymmetricKeys.cs b/src/Core/KeyManagement/Models/Data/UserAsymmetricKeys.cs new file mode 100644 index 000000000000..3c1362909260 --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/UserAsymmetricKeys.cs @@ -0,0 +1,9 @@ +#nullable enable +namespace Bit.Core.KeyManagement.Models.Data; + +public class UserAsymmetricKeys +{ + public Guid UserId { get; set; } + public required string PublicKey { get; set; } + public required string UserKeyEncryptedPrivateKey { get; set; } +} diff --git a/src/Core/KeyManagement/Repositories/IUserAsymmetricKeysRepository.cs b/src/Core/KeyManagement/Repositories/IUserAsymmetricKeysRepository.cs new file mode 100644 index 000000000000..fee9aee3bbb3 --- /dev/null +++ b/src/Core/KeyManagement/Repositories/IUserAsymmetricKeysRepository.cs @@ -0,0 +1,9 @@ +#nullable enable +using Bit.Core.KeyManagement.Models.Data; + +namespace Bit.Core.KeyManagement.Repositories; + +public interface IUserAsymmetricKeysRepository +{ + Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys); +} diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index 6cfa1ef8b34a..48e730e98de4 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Repositories; using Bit.Core.Billing.Repositories; +using Bit.Core.KeyManagement.Repositories; using Bit.Core.NotificationCenter.Repositories; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; @@ -9,6 +10,7 @@ using Bit.Infrastructure.Dapper.AdminConsole.Repositories; using Bit.Infrastructure.Dapper.Auth.Repositories; using Bit.Infrastructure.Dapper.Billing.Repositories; +using Bit.Infrastructure.Dapper.KeyManagement.Repositories; using Bit.Infrastructure.Dapper.NotificationCenter.Repositories; using Bit.Infrastructure.Dapper.Repositories; using Bit.Infrastructure.Dapper.SecretsManager.Repositories; @@ -58,6 +60,7 @@ public static void AddDapperRepositories(this IServiceCollection services, bool services.AddSingleton(); services .AddSingleton(); + services.AddSingleton(); if (selfHosted) { diff --git a/src/Infrastructure.Dapper/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs b/src/Infrastructure.Dapper/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs new file mode 100644 index 000000000000..f176327f4ff8 --- /dev/null +++ b/src/Infrastructure.Dapper/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs @@ -0,0 +1,36 @@ +#nullable enable +using System.Data; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; + +namespace Bit.Infrastructure.Dapper.KeyManagement.Repositories; + +public class UserAsymmetricKeysRepository : BaseRepository, IUserAsymmetricKeysRepository +{ + public UserAsymmetricKeysRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { + } + + public UserAsymmetricKeysRepository(string connectionString, string readOnlyConnectionString) : base( + connectionString, readOnlyConnectionString) + { + } + + public async Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys) + { + await using var connection = new SqlConnection(ConnectionString); + + await connection.ExecuteAsync("[dbo].[UserAsymmetricKeys_Regenerate]", + new + { + userAsymmetricKeys.UserId, + userAsymmetricKeys.PublicKey, + PrivateKey = userAsymmetricKeys.UserKeyEncryptedPrivateKey + }, commandType: CommandType.StoredProcedure); + } +} diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index ad0b46277b78..6b6c515e0fe3 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Billing.Repositories; using Bit.Core.Enums; +using Bit.Core.KeyManagement.Repositories; using Bit.Core.NotificationCenter.Repositories; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; @@ -10,6 +11,7 @@ using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; using Bit.Infrastructure.EntityFramework.Auth.Repositories; using Bit.Infrastructure.EntityFramework.Billing.Repositories; +using Bit.Infrastructure.EntityFramework.KeyManagement.Repositories; using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.SecretsManager.Repositories; @@ -95,6 +97,7 @@ public static void AddPasswordManagerEFRepositories(this IServiceCollection serv services.AddSingleton(); services .AddSingleton(); + services.AddSingleton(); if (selfHosted) { diff --git a/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs b/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs new file mode 100644 index 000000000000..c680424f56a2 --- /dev/null +++ b/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs @@ -0,0 +1,34 @@ +#nullable enable +using AutoMapper; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Infrastructure.EntityFramework.KeyManagement.Repositories; + +public class UserAsymmetricKeysRepository : BaseEntityFrameworkRepository, IUserAsymmetricKeysRepository +{ + public UserAsymmetricKeysRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base( + serviceScopeFactory, + mapper) + { + } + + public async Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + + var entity = await dbContext.Users.FindAsync(userAsymmetricKeys.UserId); + if (entity != null) + { + var utcNow = DateTime.UtcNow; + entity.PublicKey = userAsymmetricKeys.PublicKey; + entity.PrivateKey = userAsymmetricKeys.UserKeyEncryptedPrivateKey; + entity.RevisionDate = utcNow; + entity.AccountRevisionDate = utcNow; + await dbContext.SaveChangesAsync(); + } + } +} diff --git a/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql b/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql new file mode 100644 index 000000000000..26d0c40183bb --- /dev/null +++ b/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql @@ -0,0 +1,16 @@ +CREATE PROCEDURE [dbo].[UserAsymmetricKeys_Regenerate] + @UserId UNIQUEIDENTIFIER, + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + DECLARE @UtcNow DATETIME2(7) = GETUTCDATE(); + + UPDATE [dbo].[User] + SET [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, + [RevisionDate] = @UtcNow, + [AccountRevisionDate] = @UtcNow + WHERE [Id] = @UserId +END diff --git a/util/Migrator/DbScripts/2024-10-22_00_AddUserAsymmetricKeysRegenerate.sql b/util/Migrator/DbScripts/2024-10-22_00_AddUserAsymmetricKeysRegenerate.sql new file mode 100644 index 000000000000..e1f5431145eb --- /dev/null +++ b/util/Migrator/DbScripts/2024-10-22_00_AddUserAsymmetricKeysRegenerate.sql @@ -0,0 +1,16 @@ +CREATE OR ALTER PROCEDURE [dbo].[UserAsymmetricKeys_Regenerate] + @UserId UNIQUEIDENTIFIER, + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + DECLARE @UtcNow DATETIME2(7) = GETUTCDATE(); + + UPDATE [dbo].[User] + SET [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, + [RevisionDate] = @UtcNow, + [AccountRevisionDate] = @UtcNow + WHERE [Id] = @UserId +END