Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -121,6 +121,7 @@ public async Task RotateUserAccountKeysAsync([FromBody] RotateUserAccountKeysAnd
OrganizationUsers = await _organizationUserValidator.ValidateAsync(user, model.AccountUnlockData.OrganizationAccountRecoveryUnlockData),
WebAuthnKeys = await _webauthnKeyValidator.ValidateAsync(user, model.AccountUnlockData.PasskeyUnlockData),
DeviceKeys = await _deviceValidator.ValidateAsync(user, model.AccountUnlockData.DeviceKeyUnlockData),
V2UpgradeToken = model.AccountUnlockData.V2UpgradeToken?.ToData(),

Ciphers = await _cipherValidator.ValidateAsync(user, model.AccountData.Ciphers),
Folders = await _folderValidator.ValidateAsync(user, model.AccountData.Folders),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ public class UnlockDataRequestModel
public required IEnumerable<ResetPasswordWithOrgIdRequestModel> OrganizationAccountRecoveryUnlockData { get; set; }
public required IEnumerable<WebAuthnLoginRotateKeyRequestModel> PasskeyUnlockData { get; set; }
public required IEnumerable<OtherDeviceKeysUpdateRequestModel> DeviceKeyUnlockData { get; set; }
public V2UpgradeTokenRequestModel? V2UpgradeToken { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
ο»Ώusing System.ComponentModel.DataAnnotations;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;

namespace Bit.Api.KeyManagement.Models.Requests;

/// <summary>
/// Request model for V2 upgrade token submitted during key rotation.
/// Contains wrapped user keys allowing clients to unlock after V1β†’V2 upgrade.
/// </summary>
public class V2UpgradeTokenRequestModel
{
/// <summary>
/// User Key V2 Wrapped User Key V1.
/// </summary>
[Required]
[EncryptedString]
public required string WrappedUserKey1 { get; init; }

/// <summary>
/// User Key V1 Wrapped User Key V2.
/// </summary>
[Required]
[EncryptedString]
public required string WrappedUserKey2 { get; init; }

public V2UpgradeTokenData ToData()
{
return new V2UpgradeTokenData
{
WrappedUserKey1 = WrappedUserKey1,
WrappedUserKey2 = WrappedUserKey2
};
}
}
9 changes: 8 additions & 1 deletion src/Api/Vault/Models/Response/SyncResponseModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,14 @@ public SyncResponseModel(
Salt = user.Email.ToLowerInvariant()
}
: null,
WebAuthnPrfOptions = webAuthnPrfOptions.Length > 0 ? webAuthnPrfOptions : null
WebAuthnPrfOptions = webAuthnPrfOptions.Length > 0 ? webAuthnPrfOptions : null,
V2UpgradeToken = V2UpgradeTokenData.FromJson(user.V2UpgradeToken) is { } data
? new V2UpgradeTokenResponseModel
{
WrappedUserKey1 = data.WrappedUserKey1,
WrappedUserKey2 = data.WrappedUserKey2
}
: null
};
}

Expand Down
1 change: 1 addition & 0 deletions src/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ public static class FeatureFlagKeys
public const string EnableAccountEncryptionV2KeyConnectorRegistration = "enable-account-encryption-v2-key-connector-registration";
public const string SdkKeyRotation = "pm-30144-sdk-key-rotation";
public const string UnlockViaSdk = "unlock-via-sdk";
public const string NoLogoutOnKeyUpgradeRotation = "pm-31050-no-logout-key-upgrade-rotation";
public const string EnableAccountEncryptionV2JitPasswordRegistration = "enable-account-encryption-v2-jit-password-registration";

/* Mobile Team */
Expand Down
5 changes: 5 additions & 0 deletions src/Core/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
public DateTime? LastKeyRotationDate { get; set; }
public DateTime? LastEmailChangeDate { get; set; }
public bool VerifyDevices { get; set; } = true;
/// <summary>
/// V2 upgrade token stored as JSON containing two wrapped user keys.
/// Allows clients to unlock vault after V1 to V2 key rotation without logout.
/// </summary>
public string? V2UpgradeToken { get; set; }
// PM-28827 Uncomment below line.
// public string? MasterPasswordSalt { get; set; }

Expand Down
3 changes: 2 additions & 1 deletion src/Core/Enums/PushNotificationLogOutReason.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

public enum PushNotificationLogOutReason : byte
{
KdfChange = 0
KdfChange = 0,
KeyRotation = 1
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@ public class UserDecryptionResponseModel
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public WebAuthnPrfDecryptionOption[]? WebAuthnPrfOptions { get; set; }

/// <summary>
/// V2 upgrade token returned when available, allowing unlock after V1β†’V2 upgrade.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public V2UpgradeTokenResponseModel? V2UpgradeToken { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ο»Ώnamespace Bit.Core.KeyManagement.Models.Api.Response;

public class V2UpgradeTokenResponseModel
{
public required string WrappedUserKey1 { get; set; }
public required string WrappedUserKey2 { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class RotateUserAccountKeysData
public required IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; }
public required IEnumerable<WebAuthnLoginRotateKeyData> WebAuthnKeys { get; set; }
public required IEnumerable<Device> DeviceKeys { get; set; }
public V2UpgradeTokenData? V2UpgradeToken { get; set; }

// User vault data encrypted by the userkey
public required IEnumerable<Cipher> Ciphers { get; set; }
Expand Down
31 changes: 31 additions & 0 deletions src/Core/KeyManagement/Models/Data/V2UpgradeTokenData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
ο»Ώusing System.Text.Json;

namespace Bit.Core.KeyManagement.Models.Data;

public class V2UpgradeTokenData
{
public required string WrappedUserKey1 { get; init; }
public required string WrappedUserKey2 { get; init; }

public string ToJson()
{
return JsonSerializer.Serialize(this);
}

public static V2UpgradeTokenData? FromJson(string? json)
{
if (string.IsNullOrWhiteSpace(json))
{
return null;
}

try
{
return JsonSerializer.Deserialize<V2UpgradeTokenData>(json);
}
catch (JsonException)
{
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,16 @@ public async Task<IdentityResult> RotateUserAccountKeysAsync(User user, RotateUs
var now = DateTime.UtcNow;
user.RevisionDate = user.AccountRevisionDate = now;
user.LastKeyRotationDate = now;
user.SecurityStamp = Guid.NewGuid().ToString();

if (model.V2UpgradeToken == null)
{
user.V2UpgradeToken = null;
user.SecurityStamp = Guid.NewGuid().ToString();
}
else
{
user.V2UpgradeToken = model.V2UpgradeToken.ToJson();
}

List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = [];

Expand All @@ -99,7 +108,17 @@ public async Task<IdentityResult> RotateUserAccountKeysAsync(User user, RotateUs
UpdateUserData(model, user, saveEncryptedDataActions);

await _userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions);
await _pushService.PushLogOutAsync(user.Id);

if (model.V2UpgradeToken != null)
{
await _pushService.PushLogOutAsync(user.Id,
reason: PushNotificationLogOutReason.KeyRotation);
}
else
{
await _pushService.PushLogOutAsync(user.Id);
}

return IdentityResult.Success;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,12 @@ public async Task UpdateUserKeyAndEncryptedDataV2Async(Core.Entities.User user,
userEntity.AccountRevisionDate = user.AccountRevisionDate;
userEntity.RevisionDate = user.RevisionDate;

userEntity.SignedPublicKey = user.SignedPublicKey;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oof. Makes me wonder if we should QA V2 rotations on EF separately?

userEntity.SecurityState = user.SecurityState;
userEntity.SecurityVersion = user.SecurityVersion;

userEntity.V2UpgradeToken = user.V2UpgradeToken;

await dbContext.SaveChangesAsync();

// Update re-encrypted data
Expand Down
9 changes: 6 additions & 3 deletions src/Sql/dbo/Stored Procedures/User_Create.sql
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
@VerifyDevices BIT = 1,
@SecurityState VARCHAR(MAX) = NULL,
@SecurityVersion INT = NULL,
@SignedPublicKey VARCHAR(MAX) = NULL
@SignedPublicKey VARCHAR(MAX) = NULL,
@V2UpgradeToken VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
Expand Down Expand Up @@ -97,7 +98,8 @@ BEGIN
[SecurityState],
[SecurityVersion],
[SignedPublicKey],
[MaxStorageGbIncreased]
[MaxStorageGbIncreased],
[V2UpgradeToken]
)
VALUES
(
Expand Down Expand Up @@ -147,6 +149,7 @@ BEGIN
@SecurityState,
@SecurityVersion,
@SignedPublicKey,
@MaxStorageGb
@MaxStorageGb,
@V2UpgradeToken
)
END
6 changes: 4 additions & 2 deletions src/Sql/dbo/Stored Procedures/User_Update.sql
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
@VerifyDevices BIT = 1,
@SecurityState VARCHAR(MAX) = NULL,
@SecurityVersion INT = NULL,
@SignedPublicKey VARCHAR(MAX) = NULL
@SignedPublicKey VARCHAR(MAX) = NULL,
@V2UpgradeToken VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
Expand Down Expand Up @@ -97,7 +98,8 @@ BEGIN
[SecurityState] = @SecurityState,
[SecurityVersion] = @SecurityVersion,
[SignedPublicKey] = @SignedPublicKey,
[MaxStorageGbIncreased] = @MaxStorageGb
[MaxStorageGbIncreased] = @MaxStorageGb,
[V2UpgradeToken] = @V2UpgradeToken
WHERE
[Id] = @Id
END
5 changes: 3 additions & 2 deletions src/Sql/dbo/Tables/User.sql
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@
[LastKeyRotationDate] DATETIME2 (7) NULL,
[LastEmailChangeDate] DATETIME2 (7) NULL,
[VerifyDevices] BIT DEFAULT ((1)) NOT NULL,
[SecurityState] VARCHAR (MAX) NULL,
[SecurityState] VARCHAR (MAX) NULL,
[SecurityVersion] INT NULL,
[SignedPublicKey] VARCHAR (MAX) NULL,
[SignedPublicKey] VARCHAR (MAX) NULL,
[MaxStorageGbIncreased] SMALLINT NULL,
[V2UpgradeToken] VARCHAR(MAX) NULL,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)
);

Expand Down
3 changes: 2 additions & 1 deletion src/Sql/dbo/Views/UserView.sql
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ SELECT
[VerifyDevices],
[SecurityState],
[SecurityVersion],
[SignedPublicKey]
[SignedPublicKey],
[V2UpgradeToken]
FROM
[dbo].[User]
Loading
Loading