Skip to content

Commit

Permalink
Trusted Device Encryption feature (#3151)
Browse files Browse the repository at this point in the history
* [PM-1203] feat: allow verification for all passwordless accounts (#3038)

* [PM-1033] Org invite user creation flow 1 (#3028)

* [PM-1033] feat: remove user verification from password enrollment

* [PM-1033] feat: auto accept invitation when enrolling into password reset

* [PM-1033] fix: controller tests

* [PM-1033] refactor: `UpdateUserResetPasswordEnrollmentCommand`

* [PM-1033] refactor(wip): make `AcceptUserCommand`

* Revert "[PM-1033] refactor(wip): make `AcceptUserCommand`"

This reverts commit dc1319e.

* Revert "[PM-1033] refactor: `UpdateUserResetPasswordEnrollmentCommand`"

This reverts commit 43df689.

* [PM-1033] refactor: move invite accept to controller

This avoids creating yet another method that depends on having `IUserService` passed in as a parameter

* [PM-1033] fix: add missing changes

* [PM-1381] Add Trusted Device Keys to Auth Response (#3066)

* Return Keys for Trusted Device

- Check whether the current logging in device is trusted
- Return their keys on successful login

* Formatting

* Address PR Feedback

* Add Remarks Comment

* [PM-1338] `AuthRequest` Event Logs (#3046)

* Update AuthRequestController

- Only allow AdminApproval Requests to be created from authed endpoint
- Add endpoint that has authentication to be able to create admin approval

* Add PasswordlessAuthSettings

- Add settings for customizing expiration times

* Add new EventTypes

* Add Logic for AdminApproval Type

- Add logic for validating AdminApproval expiration
- Add event logging for Approval/Disapproval of AdminApproval
- Add logic for creating AdminApproval types

* Add Test Helpers

- Change BitAutoData to allow you to use string representations of common types.

* Add/Update AuthRequestService Tests

* Run Formatting

* Switch to 7 Days

* Add Test Covering ResponseDate Being Set

* Address PR Feedback

- Create helper for checking if date is expired
- Move validation logic into smaller methods

* Switch to User Event Type

- Make RequestDeviceApproval user type
- User types will log for each org user is in

* [PM-2998] Move Approving Device Check (#3101)

* Move Check for Approving Devices

- Exclude currently logging in device
- Remove old way of checking
- Add tests asserting behavior

* Update DeviceType list

* Update Naming & Address PR Feedback

* Fix Tests

* Address PR Feedback

* Formatting

* Now Fully Update Naming?

* Feature/auth/pm 2759/add can reset password to user decryption options (#3113)

* PM-2759 - BaseRequestValidator.cs - CreateUserDecryptionOptionsAsync - Add new hasManageResetPasswordPermission for post SSO redirect logic required on client.

* PM-2759 - Update IdentityServerSsoTests.cs to all pass based on the addition of HasManageResetPasswordPermission to TrustedDeviceUserDecryptionOption

* IdentityServerSsoTests.cs - fix typo in test name:  LoggingApproval --> LoginApproval

* PM1259 - Add test case for verifying that TrustedDeviceOption.hasManageResetPasswordPermission is set properly based on user permission

* dotnet format run

* Feature/auth/pm 2759/add can reset password to user decryption options fix jit users (#3120)

* PM-2759 - IdentityServer - CreateUserDecryptionOptionsAsync - hasManageResetPasswordPermission set logic was broken for JIT provisioned users as I assumed we would always have a list of at least 1 org during the SSO process. Added TODO for future test addition but getting this out there now as QA is blocked by being unable to create JIT provisioned users.

* dotnet format

* Tiny tweak

* [PM-1339] Allow Rotating Device Keys (#3096)

* Allow Rotation of Trusted Device Keys

- Add endpoint for getting keys relating to rotation
- Add endpoint for rotating your current device
- In the same endpoint allow a list of other devices to rotate

* Formatting

* Use Extension Method

* Add Tests from PR

Co-authored-by: Jared Snider <[email protected]>

---------

Co-authored-by: Jared Snider <[email protected]>

* Check the user directly if they have the ResetPasswordKey (#3153)

* PM-3327 - UpdateKeyAsync must exempt the currently calling device from the logout notification in order to prevent prematurely logging the user out before the client side key rotation process can complete. The calling device will log itself out once it is done. (#3170)

* Allow OTP Requests When Users Are On TDE (#3184)

* [PM-3356][PM-3292] Allow OTP For All (#3188)

* Allow OTP For All

- On a trusted device isn't a good check because a user might be using a trusted device locally but not trusted it long term
- The logic wasn't working for KC users anyways

* Remove Old Comment

* [AC-1601] Added RequireSso policy as a dependency of TDE (#3209)

* Added RequireSso policy as a dependency of TDE.

* Added test for RequireSso for TDE.

* Added save.

* Fixed policy name.

---------

Co-authored-by: Andreas Coroiu <[email protected]>
Co-authored-by: Justin Baur <[email protected]>
Co-authored-by: Vincent Salucci <[email protected]>
Co-authored-by: Jared Snider <[email protected]>
Co-authored-by: Jared Snider <[email protected]>
(cherry picked from commit 1c3afcd)
  • Loading branch information
trmartin4 committed Aug 17, 2023
1 parent 8cd2d00 commit a811740
Show file tree
Hide file tree
Showing 33 changed files with 1,666 additions and 254 deletions.
13 changes: 13 additions & 0 deletions src/Api/Auth/Controllers/AuthRequestsController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Bit.Api.Auth.Models.Response;
using Bit.Api.Models.Response;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
using Bit.Core.Auth.Services;
using Bit.Core.Exceptions;
Expand Down Expand Up @@ -72,6 +73,18 @@ public async Task<AuthRequestResponseModel> GetResponse(Guid id, [FromQuery] str
[HttpPost("")]
[AllowAnonymous]
public async Task<AuthRequestResponseModel> Post([FromBody] AuthRequestCreateRequestModel model)
{
if (model.Type == AuthRequestType.AdminApproval)
{
throw new BadRequestException("You must be authenticated to create a request of that type.");
}
var authRequest = await _authRequestService.CreateAuthRequestAsync(model);
var r = new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
return r;
}

[HttpPost("admin-request")]
public async Task<AuthRequestResponseModel> PostAdminRequest([FromBody] AuthRequestCreateRequestModel model)
{
var authRequest = await _authRequestService.CreateAuthRequestAsync(model);
var r = new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
Expand Down
14 changes: 14 additions & 0 deletions src/Api/Auth/Models/Request/UpdateDevicesTrustRequestModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Core.Auth.Models.Api.Request;

#nullable enable

namespace Bit.Api.Auth.Models.Request;

public class UpdateDevicesTrustRequestModel : SecretVerificationRequestModel
{
[Required]
public DeviceKeysUpdateRequestModel CurrentDevice { get; set; } = null!;
public IEnumerable<OtherDeviceKeysUpdateRequestModel>? OtherDevices { get; set; }
}
8 changes: 0 additions & 8 deletions src/Api/Controllers/AccountsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -879,10 +879,6 @@ public async Task PutUpdateTempPasswordAsync([FromBody] UpdateTempPasswordReques
public async Task PostRequestOTP()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user is not { UsesKeyConnector: true })
{
throw new UnauthorizedAccessException();
}

await _userService.SendOTPAsync(user);
}
Expand All @@ -891,10 +887,6 @@ public async Task PostRequestOTP()
public async Task VerifyOTP([FromBody] VerifyOTPRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user is not { UsesKeyConnector: true })
{
throw new UnauthorizedAccessException();
}

if (!await _userService.VerifyOTPAsync(user, model.OTP))
{
Expand Down
71 changes: 59 additions & 12 deletions src/Api/Controllers/DevicesController.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
using Bit.Api.Models.Request;
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Models.Request;
using Bit.Api.Models.Response;
using Bit.Core.Auth.Models.Api.Request;
using Bit.Core.Auth.Models.Api.Response;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
Expand All @@ -19,17 +23,20 @@ public class DevicesController : Controller
private readonly IDeviceService _deviceService;
private readonly IUserService _userService;
private readonly IUserRepository _userRepository;
private readonly ICurrentContext _currentContext;

public DevicesController(
IDeviceRepository deviceRepository,
IDeviceService deviceService,
IUserService userService,
IUserRepository userRepository)
IUserRepository userRepository,
ICurrentContext currentContext)
{
_deviceRepository = deviceRepository;
_deviceService = deviceService;
_userService = userService;
_userRepository = userRepository;
_currentContext = currentContext;
}

[HttpGet("{id}")]
Expand Down Expand Up @@ -66,15 +73,6 @@ public async Task<ListResponseModel<DeviceResponseModel>> Get()
return new ListResponseModel<DeviceResponseModel>(responses);
}

[HttpPost("exist-by-types")]
public async Task<ActionResult<bool>> GetExistenceByTypes([FromBody] DeviceType[] deviceTypes)
{
var userId = _userService.GetProperUserId(User).Value;
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
var userHasDeviceOfTypes = devices.Any(d => deviceTypes.Contains(d.Type));
return Ok(userHasDeviceOfTypes);
}

[HttpPost("")]
public async Task<DeviceResponseModel> Post([FromBody] DeviceRequestModel model)
{
Expand Down Expand Up @@ -117,6 +115,55 @@ public async Task<DeviceResponseModel> PutKeys(string identifier, [FromBody] Dev
return response;
}

[HttpPost("{identifier}/retrieve-keys")]
public async Task<ProtectedDeviceResponseModel> GetDeviceKeys(string identifier, [FromBody] SecretVerificationRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);

if (user == null)
{
throw new UnauthorizedAccessException();
}

if (!await _userService.VerifySecretAsync(user, model.Secret))
{
await Task.Delay(2000);
throw new BadRequestException(string.Empty, "User verification failed.");
}

var device = await _deviceRepository.GetByIdentifierAsync(identifier, user.Id);

if (device == null)
{
throw new NotFoundException();
}

return new ProtectedDeviceResponseModel(device);
}

[HttpPost("update-trust")]
public async Task PostUpdateTrust([FromBody] UpdateDevicesTrustRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);

if (user == null)
{
throw new UnauthorizedAccessException();
}

if (!await _userService.VerifySecretAsync(user, model.Secret))
{
await Task.Delay(2000);
throw new BadRequestException(string.Empty, "User verification failed.");
}

await _deviceService.UpdateDevicesTrustAsync(
_currentContext.DeviceIdentifier,
user.Id,
model.CurrentDevice,
model.OtherDevices ?? Enumerable.Empty<OtherDeviceKeysUpdateRequestModel>());
}

[HttpPut("identifier/{identifier}/token")]
[HttpPost("identifier/{identifier}/token")]
public async Task PutToken(string identifier, [FromBody] DeviceTokenRequestModel model)
Expand Down
23 changes: 10 additions & 13 deletions src/Api/Controllers/OrganizationUsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using Bit.Api.Models.Response;
using Bit.Api.Models.Response.Organizations;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
Expand Down Expand Up @@ -313,24 +312,22 @@ public async Task PutGroups(string orgId, string id, [FromBody] OrganizationUser
}

[HttpPut("{userId}/reset-password-enrollment")]
public async Task PutResetPasswordEnrollment(string orgId, string userId, [FromBody] OrganizationUserResetPasswordEnrollmentRequestModel model)
public async Task PutResetPasswordEnrollment(Guid orgId, Guid userId, [FromBody] OrganizationUserResetPasswordEnrollmentRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}

if (model.ResetPasswordKey != null && !await _userService.VerifySecretAsync(user, model.Secret))
{
await Task.Delay(2000);
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
}
else
var callingUserId = user.Id;
await _organizationService.UpdateUserResetPasswordEnrollmentAsync(
orgId, userId, model.ResetPasswordKey, callingUserId);

var orgUser = await _organizationUserRepository.GetByOrganizationAsync(orgId, user.Id);
if (orgUser.Status == OrganizationUserStatusType.Invited)
{
var callingUserId = user.Id;
await _organizationService.UpdateUserResetPasswordEnrollmentAsync(
new Guid(orgId), new Guid(userId), model.ResetPasswordKey, callingUserId);
await _organizationService.AcceptUserAsync(orgId, user, _userService);
}
}

Expand Down Expand Up @@ -466,7 +463,7 @@ public async Task BulkEnableSecretsManagerAsync(Guid orgId,
private async Task RestoreOrRevokeUserAsync(
Guid orgId,
Guid id,
Func<OrganizationUser, Guid?, Task> statusAction)
Func<Core.Entities.OrganizationUser, Guid?, Task> statusAction)
{
if (!await _currentContext.ManageUsers(orgId))
{
Expand All @@ -486,7 +483,7 @@ private async Task RestoreOrRevokeUserAsync(
private async Task<ListResponseModel<OrganizationUserBulkResponseModel>> RestoreOrRevokeUsersAsync(
Guid orgId,
OrganizationUserBulkRequestModel model,
Func<Guid, IEnumerable<Guid>, Guid?, Task<List<Tuple<OrganizationUser, string>>>> statusAction)
Func<Guid, IEnumerable<Guid>, Guid?, Task<List<Tuple<Core.Entities.OrganizationUser, string>>>> statusAction)
{
if (!await _currentContext.ManageUsers(orgId))
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
Expand Down Expand Up @@ -108,7 +107,7 @@ public class OrganizationUserUpdateGroupsRequestModel
public IEnumerable<string> GroupIds { get; set; }
}

public class OrganizationUserResetPasswordEnrollmentRequestModel : SecretVerificationRequestModel
public class OrganizationUserResetPasswordEnrollmentRequestModel
{
public string ResetPasswordKey { get; set; }
}
Expand Down
11 changes: 4 additions & 7 deletions src/Api/Models/Response/DeviceResponseModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Auth.Utilities;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Api;

Expand All @@ -19,17 +20,13 @@ public DeviceResponseModel(Device device)
Type = device.Type;
Identifier = device.Identifier;
CreationDate = device.CreationDate;
EncryptedUserKey = device.EncryptedUserKey;
EncryptedPublicKey = device.EncryptedPublicKey;
EncryptedPrivateKey = device.EncryptedPrivateKey;
IsTrusted = device.IsTrusted();
}

public Guid Id { get; set; }
public string Name { get; set; }
public DeviceType Type { get; set; }
public string Identifier { get; set; }
public DateTime CreationDate { get; set; }
public string EncryptedUserKey { get; }
public string EncryptedPublicKey { get; }
public string EncryptedPrivateKey { get; }
public bool IsTrusted { get; set; }
}
21 changes: 21 additions & 0 deletions src/Core/Auth/Models/Api/Request/DeviceKeysUpdateRequestModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;

namespace Bit.Core.Auth.Models.Api.Request;

public class OtherDeviceKeysUpdateRequestModel : DeviceKeysUpdateRequestModel
{
[Required]
public Guid DeviceId { get; set; }
}

public class DeviceKeysUpdateRequestModel
{
[Required]
[EncryptedString]
public string EncryptedPublicKey { get; set; }

[Required]
[EncryptedString]
public string EncryptedUserKey { get; set; }
}
30 changes: 30 additions & 0 deletions src/Core/Auth/Models/Api/Response/ProtectedDeviceResponseModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Api;

namespace Bit.Core.Auth.Models.Api.Response;

public class ProtectedDeviceResponseModel : ResponseModel
{
public ProtectedDeviceResponseModel(Device device)
: base("protectedDevice")
{
ArgumentNullException.ThrowIfNull(device);

Id = device.Id;
Name = device.Name;
Type = device.Type;
Identifier = device.Identifier;
CreationDate = device.CreationDate;
EncryptedUserKey = device.EncryptedUserKey;
EncryptedPublicKey = device.EncryptedPublicKey;
}

public Guid Id { get; set; }
public string Name { get; set; }
public DeviceType Type { get; set; }
public string Identifier { get; set; }
public DateTime CreationDate { get; set; }
public string EncryptedUserKey { get; set; }
public string EncryptedPublicKey { get; set; }
}
16 changes: 14 additions & 2 deletions src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,22 @@ public UserDecryptionOptions() : base("userDecryptionOptions")
public class TrustedDeviceUserDecryptionOption
{
public bool HasAdminApproval { get; }

public TrustedDeviceUserDecryptionOption(bool hasAdminApproval)
public bool HasLoginApprovingDevice { get; }
public bool HasManageResetPasswordPermission { get; }
public string? EncryptedPrivateKey { get; }
public string? EncryptedUserKey { get; }

public TrustedDeviceUserDecryptionOption(bool hasAdminApproval,
bool hasLoginApprovingDevice,
bool hasManageResetPasswordPermission,
string? encryptedPrivateKey,
string? encryptedUserKey)
{
HasAdminApproval = hasAdminApproval;
HasLoginApprovingDevice = hasLoginApprovingDevice;
HasManageResetPasswordPermission = hasManageResetPasswordPermission;
EncryptedPrivateKey = encryptedPrivateKey;
EncryptedUserKey = encryptedUserKey;
}
}

Expand Down
Loading

0 comments on commit a811740

Please sign in to comment.