Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-13362] Add private key regeneration endpoint #4929

Merged
merged 28 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a6f2118
Add stored procedure
Thomas-Avery Oct 15, 2024
4f49c0b
Add repository
Thomas-Avery Oct 15, 2024
2cb6045
Update SQL stored procedure
Thomas-Avery Oct 15, 2024
8a1c761
Bump revision dates
Thomas-Avery Oct 16, 2024
9278677
fix dapper call
Thomas-Avery Oct 16, 2024
5554adb
Merge branch 'main' into km/pm-13706/private-key-regen-db
Thomas-Avery Oct 16, 2024
88f2b97
Merge branch 'main' into km/pm-13706/private-key-regen-db
Thomas-Avery Oct 22, 2024
34bb84c
Add new RegenerateUserAsymmetricKeysCommand
Thomas-Avery Oct 22, 2024
5784903
add new command tests
Thomas-Avery Oct 22, 2024
3840b5c
Add regen controller
Thomas-Avery Oct 22, 2024
7d68d70
Add regen controller tests
Thomas-Avery Oct 22, 2024
9226389
Merge branch 'km/pm-13706/private-key-regen-db' into km/pm-13362/privโ€ฆ
Thomas-Avery Oct 22, 2024
bcd4e88
bump migration date
Thomas-Avery Oct 22, 2024
12da983
Merge branch 'km/pm-13706/private-key-regen-db' into km/pm-13362/privโ€ฆ
Thomas-Avery Oct 22, 2024
0b60bb9
add feature flag
Thomas-Avery Oct 23, 2024
292fa3e
Add more logging
Thomas-Avery Oct 24, 2024
4d6ae57
update request model
Thomas-Avery Oct 25, 2024
23d9e3f
update tests
Thomas-Avery Oct 25, 2024
4640ad8
Merge branch 'main' into km/pm-13706/private-key-regen-db
Thomas-Avery Nov 13, 2024
ff8e01e
bump migration date
Thomas-Avery Nov 13, 2024
3ffa555
Merge branch 'km/pm-13706/private-key-regen-db' into km/pm-13362/privโ€ฆ
Thomas-Avery Nov 13, 2024
6899e49
Add push notification to sync new asymmetric keys to other devices
Thomas-Avery Nov 14, 2024
58bf726
Merge branch 'main' into km/pm-13362/private-key-regen-endpoint
Thomas-Avery Nov 21, 2024
e38fb90
remove old migration
Thomas-Avery Nov 21, 2024
22f326d
Merge branch 'main' into km/pm-13362/private-key-regen-endpoint
Thomas-Avery Dec 11, 2024
fec5397
Merge branch 'main' into km/pm-13362/private-key-regen-endpoint
Thomas-Avery Dec 13, 2024
3f00218
Merge branch 'main' into km/pm-13362/private-key-regen-endpoint
Thomas-Avery Dec 16, 2024
2927a26
Merge branch 'main' into km/pm-13362/private-key-regen-endpoint
Thomas-Avery Dec 16, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
๏ปฟ#nullable enable
using Bit.Api.KeyManagement.Models.Requests;
using Bit.Core;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Commands.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Bit.Api.KeyManagement.Controllers;

[Route("accounts/key-management")]
[Authorize("Application")]
public class AccountsKeyManagementController : Controller
{
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
private readonly IFeatureService _featureService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IRegenerateUserAsymmetricKeysCommand _regenerateUserAsymmetricKeysCommand;
private readonly IUserService _userService;

public AccountsKeyManagementController(IUserService userService,
IFeatureService featureService,
IOrganizationUserRepository organizationUserRepository,
IEmergencyAccessRepository emergencyAccessRepository,
IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand)
{
_userService = userService;
_featureService = featureService;
_regenerateUserAsymmetricKeysCommand = regenerateUserAsymmetricKeysCommand;
_organizationUserRepository = organizationUserRepository;
_emergencyAccessRepository = emergencyAccessRepository;
}

[HttpPost("regenerate-keys")]
public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request)
{
if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration))
{
throw new NotFoundException();
}

var user = await _userService.GetUserByPrincipalAsync(User) ?? throw new UnauthorizedAccessException();
var usersOrganizationAccounts = await _organizationUserRepository.GetManyByUserAsync(user.Id);
var designatedEmergencyAccess = await _emergencyAccessRepository.GetManyDetailsByGranteeIdAsync(user.Id);
await _regenerateUserAsymmetricKeysCommand.RegenerateKeysAsync(request.ToUserAsymmetricKeys(user.Id),
usersOrganizationAccounts, designatedEmergencyAccess);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
๏ปฟ#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;

namespace Bit.Api.KeyManagement.Models.Requests;

public class KeyRegenerationRequestModel
{
[Required]
public required string UserPublicKey { get; set; }
Thomas-Avery marked this conversation as resolved.
Show resolved Hide resolved

[Required]
[EncryptedString]
public required string UserKeyEncryptedUserPrivateKey { get; set; }

public UserAsymmetricKeys ToUserAsymmetricKeys(Guid userId)
{
return new UserAsymmetricKeys
{
UserId = userId,
PublicKey = UserPublicKey,
UserKeyEncryptedPrivateKey = UserKeyEncryptedUserPrivateKey,
};
}
}
1 change: 1 addition & 0 deletions src/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ public static class FeatureFlagKeys
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
public const string Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions";
public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";

public static List<string> GetAllKeys()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
๏ปฟ#nullable enable
using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;

namespace Bit.Core.KeyManagement.Commands.Interfaces;

public interface IRegenerateUserAsymmetricKeysCommand
{
Task RegenerateKeysAsync(UserAsymmetricKeys userAsymmetricKeys,
ICollection<OrganizationUser> usersOrganizationAccounts,
ICollection<EmergencyAccessDetails> designatedEmergencyAccess);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
๏ปฟ#nullable enable
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Commands.Interfaces;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Repositories;
using Microsoft.Extensions.Logging;

namespace Bit.Core.KeyManagement.Commands;

public class RegenerateUserAsymmetricKeysCommand : IRegenerateUserAsymmetricKeysCommand
{
private readonly ICurrentContext _currentContext;
private readonly ILogger<RegenerateUserAsymmetricKeysCommand> _logger;
private readonly IUserAsymmetricKeysRepository _userAsymmetricKeysRepository;

public RegenerateUserAsymmetricKeysCommand(
ICurrentContext currentContext,
IUserAsymmetricKeysRepository userAsymmetricKeysRepository,
ILogger<RegenerateUserAsymmetricKeysCommand> logger)
{
_currentContext = currentContext;
_logger = logger;
_userAsymmetricKeysRepository = userAsymmetricKeysRepository;
}

public async Task RegenerateKeysAsync(UserAsymmetricKeys userAsymmetricKeys,
ICollection<OrganizationUser> usersOrganizationAccounts,
ICollection<EmergencyAccessDetails> designatedEmergencyAccess)
{
var userId = _currentContext.UserId;
if (!userId.HasValue ||
userAsymmetricKeys.UserId != userId.Value ||
usersOrganizationAccounts.Any(ou => ou.UserId != userId) ||
designatedEmergencyAccess.Any(dea => dea.GranteeId != userId))
{
throw new NotFoundException();
}

var inOrganizations = usersOrganizationAccounts.Any(ou =>
ou.Status is OrganizationUserStatusType.Confirmed or OrganizationUserStatusType.Revoked);
var hasDesignatedEmergencyAccess = designatedEmergencyAccess.Any(x =>
x.Status is EmergencyAccessStatusType.Confirmed or EmergencyAccessStatusType.RecoveryApproved
or EmergencyAccessStatusType.RecoveryInitiated);

_logger.LogInformation(
"User asymmetric keys regeneration requested. UserId: {userId} OrganizationMembership: {inOrganizations} DesignatedEmergencyAccess: {hasDesignatedEmergencyAccess} DeviceType: {deviceType}",
userAsymmetricKeys.UserId, inOrganizations, hasDesignatedEmergencyAccess, _currentContext.DeviceType);

// For now, don't regenerate asymmetric keys for user's with organization membership and designated emergency access.
if (inOrganizations || hasDesignatedEmergencyAccess)
{
throw new BadRequestException("Key regeneration not supported for this user.");
}

await _userAsymmetricKeysRepository.RegenerateUserAsymmetricKeysAsync(userAsymmetricKeys);
_logger.LogInformation(
"User's asymmetric keys regenerated. UserId: {userId} OrganizationMembership: {inOrganizations} DesignatedEmergencyAccess: {hasDesignatedEmergencyAccess} DeviceType: {deviceType}",
userAsymmetricKeys.UserId, inOrganizations, hasDesignatedEmergencyAccess, _currentContext.DeviceType);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
๏ปฟusing Bit.Core.KeyManagement.Commands;
using Bit.Core.KeyManagement.Commands.Interfaces;
using Microsoft.Extensions.DependencyInjection;

namespace Bit.Core.KeyManagement;

public static class KeyManagementServiceCollectionExtensions
{
public static void AddKeyManagementServices(this IServiceCollection services)
{
services.AddKeyManagementCommands();
}

private static void AddKeyManagementCommands(this IServiceCollection services)
{
services.AddScoped<IRegenerateUserAsymmetricKeysCommand, RegenerateUserAsymmetricKeysCommand>();
}
}
9 changes: 9 additions & 0 deletions src/Core/KeyManagement/Models/Data/UserAsymmetricKeys.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
๏ปฟ#nullable enable
using Bit.Core.KeyManagement.Models.Data;

namespace Bit.Core.KeyManagement.Repositories;

public interface IUserAsymmetricKeysRepository
{
Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -58,6 +60,7 @@ public static void AddDapperRepositories(this IServiceCollection services, bool
services.AddSingleton<INotificationStatusRepository, NotificationStatusRepository>();
services
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();

if (selfHosted)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -95,6 +97,7 @@ public static void AddPasswordManagerEFRepositories(this IServiceCollection serv
services.AddSingleton<INotificationStatusRepository, NotificationStatusRepository>();
services
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();

if (selfHosted)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
2 changes: 2 additions & 0 deletions src/SharedWeb/Utilities/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
using Bit.Core.HostedServices;
using Bit.Core.Identity;
using Bit.Core.IdentityServer;
using Bit.Core.KeyManagement;
using Bit.Core.NotificationHub;
using Bit.Core.OrganizationFeatures;
using Bit.Core.Repositories;
Expand Down Expand Up @@ -116,6 +117,7 @@ public static void AddBaseServices(this IServiceCollection services, IGlobalSett
services.AddLoginServices();
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();
services.AddVaultServices();
services.AddKeyManagementServices();
}

public static void AddTokenizers(this IServiceCollection services)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions test/Api.IntegrationTest/Helpers/LoginHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ public LoginHelper(ApiApplicationFactory factory, HttpClient client)
_client = client;
}

public async Task LoginAsync(string email)
{
var tokens = await _factory.LoginAsync(email);
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
}

public async Task LoginWithOrganizationApiKeyAsync(Guid organizationId)
{
var (clientId, apiKey) = await GetOrganizationApiKey(_factory, organizationId);
Expand Down
Loading
Loading