Browse Source
* doc: adding readme and comments to code for emergency access feature. * fix: renaming variable names to better match vocabulary around emergency access.pull/5891/head
6 changed files with 309 additions and 90 deletions
@ -0,0 +1,147 @@
@@ -0,0 +1,147 @@
|
||||
using Bit.Core.AdminConsole.Entities; |
||||
using Bit.Core.Auth.Entities; |
||||
using Bit.Core.Auth.Enums; |
||||
using Bit.Core.Auth.Models.Data; |
||||
using Bit.Core.Entities; |
||||
using Bit.Core.Enums; |
||||
using Bit.Core.Services; |
||||
using Bit.Core.Vault.Models.Data; |
||||
|
||||
namespace Bit.Core.Auth.Services; |
||||
|
||||
public interface IEmergencyAccessService |
||||
{ |
||||
/// <summary> |
||||
/// Invites a user via email to become an emergency contact for the Grantor user. The Grantor must have a premium subscription. |
||||
/// the grantor user must not be a member of the organization that uses KeyConnector. |
||||
/// </summary> |
||||
/// <param name="grantorUser">The user initiating the emergency contact request</param> |
||||
/// <param name="emergencyContactEmail">Emergency contact</param> |
||||
/// <param name="accessType">Type of emergency access allowed to the emergency contact</param> |
||||
/// <param name="waitTime">The amount of time to pass before the invite is auto confirmed</param> |
||||
/// <returns>a new Emergency Access object</returns> |
||||
Task<EmergencyAccess> InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime); |
||||
/// <summary> |
||||
/// Sends an invite to the emergency contact associated with the emergency access id. |
||||
/// </summary> |
||||
/// <param name="grantorUser">The grantor. This must be the owner of the Emergency Access object</param> |
||||
/// <param name="emergencyAccessId">The Id of the emergency access being requested.</param> |
||||
/// <returns>void</returns> |
||||
Task ResendInviteAsync(User grantorUser, Guid emergencyAccessId); |
||||
/// <summary> |
||||
/// A grantee user accepts the emergency contact request. This updates the emergency access status to be |
||||
/// "Accepted", this is the middle step before the grantor user confirms the request. |
||||
/// </summary> |
||||
/// <param name="emergencyAccessId">Id of the emergency access object being acted on.</param> |
||||
/// <param name="granteeUser">User being invited to be an emergency contact</param> |
||||
/// <param name="token">the tokenable that was sent via email</param> |
||||
/// <param name="userService">service dependency</param> |
||||
/// <returns>void</returns> |
||||
Task<EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService); |
||||
/// <summary> |
||||
/// The creator of the emergency access request can delete the request. |
||||
/// </summary> |
||||
/// <param name="emergencyAccessId">Id of the emergency access being acted on</param> |
||||
/// <param name="grantorId">Id of the owner trying to delete the emergency access request</param> |
||||
/// <returns>void</returns> |
||||
Task DeleteAsync(Guid emergencyAccessId, Guid grantorId); |
||||
/// <summary> |
||||
/// The grantor user confirms the acceptance of the emergency contact request. This stores the encrypted key allowing the grantee |
||||
/// access based on the emergency access type. |
||||
/// </summary> |
||||
/// <param name="emergencyAccessId">Id of the emergency access being acted on.</param> |
||||
/// <param name="key">The grantor user key encrypted by the grantee public key; grantee.PubicKey(grantor.User.Key)</param> |
||||
/// <param name="grantorId">Id of grantor user</param> |
||||
/// <returns>emergency access object associated with the Id passed in</returns> |
||||
Task<EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId); |
||||
/// <summary> |
||||
/// Fetches an emergency access object. The grantor user must own the object being fetched. |
||||
/// </summary> |
||||
/// <param name="emergencyAccessId">Id of emergency access object</param> |
||||
/// <param name="grantorId">Id of the owner of the emergency access object.</param> |
||||
/// <returns>Details of the emergency access object</returns> |
||||
Task<EmergencyAccessDetails> GetAsync(Guid emergencyAccessId, Guid grantorId); |
||||
/// <summary> |
||||
/// Updates the emergency access object. |
||||
/// </summary> |
||||
/// <param name="emergencyAccess">emergency access entity being updated</param> |
||||
/// <param name="grantorUser">grantor user</param> |
||||
/// <returns>void</returns> |
||||
Task SaveAsync(EmergencyAccess emergencyAccess, User grantorUser); |
||||
/// <summary> |
||||
/// Initiates the recovery process. For either Takeover or view. Will send an email to the Grantor User notifying of the initiation. |
||||
/// </summary> |
||||
/// <param name="emergencyAccessId">EmergencyAccess Id</param> |
||||
/// <param name="granteeUser">grantee user</param> |
||||
/// <returns>void</returns> |
||||
Task InitiateAsync(Guid emergencyAccessId, User granteeUser); |
||||
/// <summary> |
||||
/// Approves a recovery request. Sets the EmergencyAccess.Status to RecoveryApproved. |
||||
/// </summary> |
||||
/// <param name="emergencyAccessId">emergency access id</param> |
||||
/// <param name="grantorUser">grantor user</param> |
||||
/// <returns>void</returns> |
||||
Task ApproveAsync(Guid emergencyAccessId, User grantorUser); |
||||
/// <summary> |
||||
/// Rejects a recovery request. Sets the EmergencyAccess.Status to Confirmed. This does not remove the emergency access entity. The |
||||
/// Grantee user can still initiate another recovery request. |
||||
/// </summary> |
||||
/// <param name="emergencyAccessId">emergency access id</param> |
||||
/// <param name="grantorUser">grantor user</param> |
||||
/// <returns>void</returns> |
||||
Task RejectAsync(Guid emergencyAccessId, User grantorUser); |
||||
/// <summary> |
||||
/// This request is made by the Grantee user to fetch the policies <see cref="Policy"/> for the Grantor User. |
||||
/// The Grantor User has to be the owner of the organization. <see cref="OrganizationUserType"/> |
||||
/// If the Grantor user has OrganizationUserType.Owner then the policies for the _Grantor_ user |
||||
/// are returned. This is used to ensure the password is of the proper complexity for the organization. |
||||
/// </summary> |
||||
/// <param name="emergencyAccessId">EmergencyAccess.Id being acted on</param> |
||||
/// <param name="granteeUser">User making the request, this is the Grantee</param> |
||||
/// <returns>null if the GrantorUser is not an organization owner; A list of policies otherwise.</returns> |
||||
Task<ICollection<Policy>> GetPoliciesAsync(Guid emergencyAccessId, User granteeUser); |
||||
/// <summary> |
||||
/// Fetches the emergency access entity and grantor user. The grantor user is returned so the correct KDF configuration is |
||||
/// used for the new password. |
||||
/// </summary> |
||||
/// <param name="emergencyAccessId">Id of entity being accessed</param> |
||||
/// <param name="granteeUser">grantee user of the emergency access entity</param> |
||||
/// <returns>emergency access entity and the grantorUser</returns> |
||||
Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser); |
||||
/// <summary> |
||||
/// Updates the grantor's password hash and updates the key for the EmergencyAccess entity. |
||||
/// </summary> |
||||
/// <param name="emergencyAccessId">Emergency Access Id being acted on</param> |
||||
/// <param name="granteeUser">user making the request</param> |
||||
/// <param name="newMasterPasswordHash">new password hash set by grantee user</param> |
||||
/// <param name="key">new encrypted user key</param> |
||||
/// <returns>void</returns> |
||||
Task PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key); |
||||
/// <summary> |
||||
/// sends a reminder email that there is a pending request for recovery. |
||||
/// </summary> |
||||
/// <returns>void</returns> |
||||
Task SendNotificationsAsync(); |
||||
/// <summary> |
||||
/// This handles the auto approval of recovery requests started in the <see cref="InitiateAsync"/> method. |
||||
/// An email will be sent to the Grantee and the Grantor notifying each the recovery has been approved. |
||||
/// </summary> |
||||
/// <returns>void</returns> |
||||
Task HandleTimedOutRequestsAsync(); |
||||
/// <summary> |
||||
/// Fetched ciphers from the grantors account for the grantee to view. |
||||
/// </summary> |
||||
/// <param name="emergencyAccessId">Emergency access entity being acted on</param> |
||||
/// <param name="granteeUser">user requesting cipher items</param> |
||||
/// <returns>ciphers associated with the emergency access request</returns> |
||||
Task<EmergencyAccessViewData> ViewAsync(Guid emergencyAccessId, User granteeUser); |
||||
/// <summary> |
||||
/// Returns attachment if the grantee user has access to the cipher through the emergency access entity. |
||||
/// </summary> |
||||
/// <param name="emergencyAccessId">EmergencyAccess entity being acted on</param> |
||||
/// <param name="cipherId">cipher entity containing the attachment</param> |
||||
/// <param name="attachmentId">Attachment entity</param> |
||||
/// <param name="granteeUser">user making the request</param> |
||||
/// <returns>attachment response </returns> |
||||
Task<AttachmentResponseData> GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser); |
||||
} |
||||
@ -0,0 +1,95 @@
@@ -0,0 +1,95 @@
|
||||
# Emergency Access System |
||||
This system allows users to share their `User.Key` with other users using public key exchange. An emergency contact (a grantee user) can view or takeover (reset the password) of the grantor user. |
||||
|
||||
When an account is taken over all two factor methods are turned off and device verification is disabled. |
||||
|
||||
This system is affected by the Key Rotation feature. The `EmergencyAccess.KeyEncrypted` is the `Grantor.UserKey` encrypted by the `Grantee.PublicKey`. So if the `User.Key` is rotated then all `EmergencyAccess` entities will need to be updated. |
||||
|
||||
## Special Cases |
||||
Users who use `KeyConnector` are not able to allow `Takeover` of their accounts. However, they can allow `View`. |
||||
|
||||
When a grantee user _takes over_ a grantor user's account, the grantor user will be removed from all organizations where the grantor user is not the `OrganizationUserType.Owner`. A grantor user will not be removed from organizations if the `EmergencyAccessType` is `View`. The grantee user will only be able to `View` the grantor user's ciphers, and not any of the organization ciphers, if any exist. |
||||
|
||||
## Step 1. Invitation |
||||
|
||||
A grantor user invites another user to be their emergency contact, the grantee. This will create a new `EmergencyAccess` entity in the database with the `EmergencyAccessStatusType` set to `Invited`. |
||||
The `EmergencyAccess.KeyEncrypted` field is empty, and the `GranteeId` is `null` since the user being invited via email may not have an account yet. |
||||
|
||||
### code |
||||
```csharp |
||||
// creates entity. |
||||
Task<EmergencyAccess> InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime); |
||||
// resend email to the EmergencyAccess.Email. |
||||
Task ResendInviteAsync(User grantorUser, Guid emergencyAccessId); |
||||
``` |
||||
|
||||
## Step 2. Acceptance |
||||
|
||||
The grantee user receives an email they have been invited to be an emergency contact for a grantor user. |
||||
|
||||
At this point the grantee user can accept the request. This will set the `EmergencyAccess.GranteeId` to the `User.Id` of the grantee user. The `EmergencyAccess.Status` is set to `Accepted`. |
||||
|
||||
If the grantee user does not have an account then they can create an account and accept the invitation. |
||||
|
||||
### Code |
||||
```csharp |
||||
// accepts the request to be an emergency contact. |
||||
Task<EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService); |
||||
``` |
||||
|
||||
## Step 3. Confirmation |
||||
|
||||
Once the grantee user has accepted, the `EmergencyAccess.GranteeId` allows the grantor user the ability to query for the `GranteeUser.PublicKey`. With the `Grantee.PublicKey`, the grantor on the client is able to safely encrypt their `User.Key` and save the encrypted string to the database. |
||||
|
||||
The `EmergencyAccess.Status` is set to `Confirmed`, and the `EmergencyAccess.KeyEncrypted` is set. |
||||
|
||||
### Code |
||||
```csharp |
||||
Task<EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId); |
||||
``` |
||||
|
||||
## Step 4. Recovery Approval |
||||
|
||||
The grantee user can now exercise the ability to view or takeover the account. This is done by initiating the recovery. Initiating recovery has a time delay specified by `EmergencyAccess.WaitTime`. `WaitTime` is set in the initial invite. The grantor user can approve the request before the `WaitTime`, but they _cannot_ reject the request _after_ the `WaitTime` has completed. If the recovery request is not rejected then once the `WaitTime` has passed the grantee user will be able to access the emergency access entity. |
||||
|
||||
### Code |
||||
```csharp |
||||
// Initiates the recovery process; Will set EmergencyAccess.Status to RecoveryInitiated. |
||||
Task InitiateAsync(Guid id, User granteeUser); |
||||
// Approved the recovery request; Will set EmergencyAccess.Status to RecoveryApproved. |
||||
Task ApproveAsync(Guid id, User approvingUser); |
||||
// Rejects the recovery request; Will set EmergencyAccess.Status to Confirmed. |
||||
Task RejectAsync(Guid id, User rejectingUser); |
||||
// Automatically set the EmergencyAccess.Status to RecoveryApproved after WaitTime has passed. |
||||
Task HandleTimedOutRequestsAsync(); |
||||
``` |
||||
|
||||
## Step 5. Recovering the account |
||||
|
||||
Once the `EmergencyAccess.Status` is `RecoveryApproved` the grantee user is able to exercise their ability to view or takeover the grantor account. Viewing allows the grantee user to view the vault data of the grantor user. Takeover allows the grantee to change the password of the grantor user. |
||||
|
||||
### Takeover |
||||
`TakeoverAsync(Guid, User)` returns the grantor user object along with the `EmergencyAccess` entity. The grantor user object is required since to update the password the client needs access to the grantor kdf configuration. Once the password has been set in the `PasswordAsync(Guid, User, string, string)` the account has been successfully recovered. |
||||
|
||||
Taking over the account will change the password of the grantor user, empty the two factor array on the grantor user, and disable device verification. |
||||
|
||||
```csharp |
||||
// Takeover returns the grantor user and the emergency access entity. |
||||
Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser); |
||||
// Password sets the password for the grantor user. |
||||
Task PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key); |
||||
// Returns Ciphers the Grantee is allowed to view based on the EmergencyAccess status. |
||||
Task<EmergencyAccessViewData> ViewAsync(Guid emergencyAccessId, User granteeUser); |
||||
// Returns downloadable cipher attachments based on the EmergencyAccess status. |
||||
Task<AttachmentResponseData> GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser); |
||||
``` |
||||
|
||||
## Optional steps |
||||
|
||||
The grantor user is able to delete an emergency contact at anytime, at any point in the recovery process. |
||||
|
||||
### Code |
||||
```csharp |
||||
// deletes the associated EmergencyAccess Entity |
||||
Task DeleteAsync(Guid emergencyAccessId, Guid grantorId); |
||||
``` |
||||
@ -1,40 +0,0 @@
@@ -1,40 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Entities; |
||||
using Bit.Core.Auth.Entities; |
||||
using Bit.Core.Auth.Enums; |
||||
using Bit.Core.Auth.Models.Data; |
||||
using Bit.Core.Entities; |
||||
using Bit.Core.Enums; |
||||
using Bit.Core.Services; |
||||
using Bit.Core.Vault.Models.Data; |
||||
|
||||
namespace Bit.Core.Auth.Services; |
||||
|
||||
public interface IEmergencyAccessService |
||||
{ |
||||
Task<EmergencyAccess> InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime); |
||||
Task ResendInviteAsync(User invitingUser, Guid emergencyAccessId); |
||||
Task<EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User user, string token, IUserService userService); |
||||
Task DeleteAsync(Guid emergencyAccessId, Guid grantorId); |
||||
Task<EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId); |
||||
Task<EmergencyAccessDetails> GetAsync(Guid emergencyAccessId, Guid userId); |
||||
Task SaveAsync(EmergencyAccess emergencyAccess, User savingUser); |
||||
Task InitiateAsync(Guid id, User initiatingUser); |
||||
Task ApproveAsync(Guid id, User approvingUser); |
||||
Task RejectAsync(Guid id, User rejectingUser); |
||||
/// <summary> |
||||
/// This request is made by the Grantee user to fetch the policies <see cref="Policy"/> for the Grantor User. |
||||
/// The Grantor User has to be the owner of the organization. <see cref="OrganizationUserType"/> |
||||
/// If the Grantor user has OrganizationUserType.Owner then the policies for the _Grantor_ user |
||||
/// are returned. |
||||
/// </summary> |
||||
/// <param name="id">EmergencyAccess.Id being acted on</param> |
||||
/// <param name="requestingUser">User making the request, this is the Grantee</param> |
||||
/// <returns>null if the GrantorUser is not an organization owner; A list of policies otherwise.</returns> |
||||
Task<ICollection<Policy>> GetPoliciesAsync(Guid id, User requestingUser); |
||||
Task<(EmergencyAccess, User)> TakeoverAsync(Guid id, User initiatingUser); |
||||
Task PasswordAsync(Guid id, User user, string newMasterPasswordHash, string key); |
||||
Task SendNotificationsAsync(); |
||||
Task HandleTimedOutRequestsAsync(); |
||||
Task<EmergencyAccessViewData> ViewAsync(Guid id, User user); |
||||
Task<AttachmentResponseData> GetAttachmentDownloadAsync(Guid id, Guid cipherId, string attachmentId, User user); |
||||
} |
||||
Loading…
Reference in new issue