mirror of
https://github.com/bitwarden/server.git
synced 2025-07-02 08:32:50 -05:00
Individual Vault Item Encryption Feature (#3256)
* [SG-966] [SG-967] Add new cipher properties, update DB objects and create migrations (#2681) * Updated cipher entity with two new columns * Added sqlserver mifgration and updated applicable stored procedures and table * Added EF Migrations * Made changes to response model to include new column properties * Fixed formatting * Modified scripts to reflect suggestions made on PR * Added column to cipher table using default * Include constraint in create cipher table script * Added key and forcerotatekey property to request model (#2716) * Added key update on the Cipher_UpdateWithCollection stored procedure, ef (#2855) * Added key and forceKeyRotation to BuildCiphersTable method (#2893) * [PM-2211] Remove forceKeyRotation column (#2921) * Removed forceKeyRotation column * Adjusted date for migrtaion file * Passed key column to update cipher script to update cipher key when it is rotated (#2967) * [PM-2448] Update CipherDetails_Update SP to update attachment column (#2992) * Updated the cipherdetails_update stored procedure to update the attachement column when encrypted with the cipher key * Moved migration and renamed old migration file * Fixed lint issues * Fixed lint issues * renamed sqlserver migration to have a more recent date * [PM-2548] Added validation to edit and add attachments methods (#3130) * PM-2548 Added validation to edit and add attachments methods * PM-2548 Moved the validation to a private method * PM-2548 Minor refactor * Bumped up minimum version * Bumped up minimum version * Changed version for tests purposes * Bumped up minimum version * Updated encryption minimum version to match clients for QA. * PM-3976 Passed Key column to update cipher on bulk edit (#3299) * Updated minimum client version in preparation for release. * Renamed migration with current date. (#3303) --------- Co-authored-by: SmithThe4th <gsmith@bitwarden.com> Co-authored-by: gbubemismith <gsmithwalter@gmail.com> Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com> Co-authored-by: Carlos Gonçalves <carlosmaccam@gmail.com>
This commit is contained in:
@ -36,6 +36,7 @@ public class CiphersController : Controller
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ILogger<CiphersController> _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly Version _cipherKeyEncryptionMinimumVersion = new Version(Constants.CipherKeyEncryptionMinimumVersion);
|
||||
|
||||
public CiphersController(
|
||||
ICipherRepository cipherRepository,
|
||||
@ -177,6 +178,8 @@ public class CiphersController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
ValidateItemLevelEncryptionIsAvailable(cipher);
|
||||
|
||||
var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id)).Select(c => c.CollectionId).ToList();
|
||||
var modelOrgId = string.IsNullOrWhiteSpace(model.OrganizationId) ?
|
||||
(Guid?)null : new Guid(model.OrganizationId);
|
||||
@ -198,6 +201,9 @@ public class CiphersController : Controller
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id);
|
||||
|
||||
ValidateItemLevelEncryptionIsAvailable(cipher);
|
||||
|
||||
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
||||
!await _currentContext.EditAnyCollection(cipher.OrganizationId.Value))
|
||||
{
|
||||
@ -576,6 +582,8 @@ public class CiphersController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
ValidateItemLevelEncryptionIsAvailable(cipher);
|
||||
|
||||
if (request.FileSize > CipherService.MAX_FILE_SIZE)
|
||||
{
|
||||
throw new BadRequestException($"Max file size is {CipherService.MAX_FILE_SIZE_READABLE}.");
|
||||
@ -795,4 +803,12 @@ public class CiphersController : Controller
|
||||
throw new BadRequestException("Invalid content.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateItemLevelEncryptionIsAvailable(Cipher cipher)
|
||||
{
|
||||
if (cipher.Key != null && _currentContext.ClientVersion < _cipherKeyEncryptionMinimumVersion)
|
||||
{
|
||||
throw new BadRequestException("Cannot edit item. Update to the latest version of Bitwarden and try again.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ public class CipherRequestModel
|
||||
public string FolderId { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public CipherRepromptType Reprompt { get; set; }
|
||||
public string Key { get; set; }
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(1000)]
|
||||
@ -86,6 +87,7 @@ public class CipherRequestModel
|
||||
}
|
||||
|
||||
existingCipher.Reprompt = Reprompt;
|
||||
existingCipher.Key = Key;
|
||||
|
||||
var hasAttachments2 = (Attachments2?.Count ?? 0) > 0;
|
||||
var hasAttachments = (Attachments?.Count ?? 0) > 0;
|
||||
|
@ -63,6 +63,7 @@ public class CipherMiniResponseModel : ResponseModel
|
||||
CreationDate = cipher.CreationDate;
|
||||
DeletedDate = cipher.DeletedDate;
|
||||
Reprompt = cipher.Reprompt.GetValueOrDefault(CipherRepromptType.None);
|
||||
Key = cipher.Key;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
@ -83,6 +84,7 @@ public class CipherMiniResponseModel : ResponseModel
|
||||
public DateTime CreationDate { get; set; }
|
||||
public DateTime? DeletedDate { get; set; }
|
||||
public CipherRepromptType Reprompt { get; set; }
|
||||
public string Key { get; set; }
|
||||
}
|
||||
|
||||
public class CipherResponseModel : CipherMiniResponseModel
|
||||
|
@ -19,6 +19,8 @@ public static class Constants
|
||||
/// their subscription has expired.
|
||||
/// </summary>
|
||||
public const int OrganizationSelfHostSubscriptionGracePeriodDays = 60;
|
||||
|
||||
public const string CipherKeyEncryptionMinimumVersion = "2023.9.1";
|
||||
}
|
||||
|
||||
public static class TokenPurposes
|
||||
|
@ -21,6 +21,7 @@ public class Cipher : ITableObject<Guid>, ICloneable
|
||||
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? DeletedDate { get; set; }
|
||||
public Enums.CipherRepromptType? Reprompt { get; set; }
|
||||
public string Key { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
|
@ -392,7 +392,8 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
||||
SET
|
||||
[Data] = TC.[Data],
|
||||
[Attachments] = TC.[Attachments],
|
||||
[RevisionDate] = TC.[RevisionDate]
|
||||
[RevisionDate] = TC.[RevisionDate],
|
||||
[Key] = TC.[Key]
|
||||
FROM
|
||||
[dbo].[Cipher] C
|
||||
INNER JOIN
|
||||
@ -506,7 +507,8 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
||||
[Data] = TC.[Data],
|
||||
[Attachments] = TC.[Attachments],
|
||||
[RevisionDate] = TC.[RevisionDate],
|
||||
[DeletedDate] = TC.[DeletedDate]
|
||||
[DeletedDate] = TC.[DeletedDate],
|
||||
[Key] = TC.[Key]
|
||||
FROM
|
||||
[dbo].[Cipher] C
|
||||
INNER JOIN
|
||||
@ -728,6 +730,8 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
||||
ciphersTable.Columns.Add(deletedDateColumn);
|
||||
var repromptColumn = new DataColumn(nameof(c.Reprompt), typeof(short));
|
||||
ciphersTable.Columns.Add(repromptColumn);
|
||||
var keyColummn = new DataColumn(nameof(c.Key), typeof(string));
|
||||
ciphersTable.Columns.Add(keyColummn);
|
||||
|
||||
foreach (DataColumn col in ciphersTable.Columns)
|
||||
{
|
||||
@ -754,6 +758,7 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
||||
row[revisionDateColumn] = cipher.RevisionDate;
|
||||
row[deletedDateColumn] = cipher.DeletedDate.HasValue ? (object)cipher.DeletedDate : DBNull.Value;
|
||||
row[repromptColumn] = cipher.Reprompt;
|
||||
row[keyColummn] = cipher.Key;
|
||||
|
||||
ciphersTable.Rows.Add(row);
|
||||
}
|
||||
|
@ -73,6 +73,7 @@ public class UserCipherDetailsQuery : IQuery<CipherDetails>
|
||||
Reprompt = c.Reprompt,
|
||||
ViewPassword = true,
|
||||
OrganizationUseTotp = false,
|
||||
Key = c.Key
|
||||
});
|
||||
return union;
|
||||
}
|
||||
|
@ -366,6 +366,7 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
|
||||
Reprompt = c.Reprompt,
|
||||
ViewPassword = true,
|
||||
OrganizationUseTotp = false,
|
||||
Key = c.Key
|
||||
};
|
||||
}
|
||||
var ciphers = await cipherDetailsView.ToListAsync();
|
||||
@ -591,6 +592,7 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
|
||||
trackedCipher.Attachments = cipher.Attachments;
|
||||
trackedCipher.RevisionDate = cipher.RevisionDate;
|
||||
trackedCipher.DeletedDate = cipher.DeletedDate;
|
||||
trackedCipher.Key = cipher.Key;
|
||||
|
||||
await transaction.CommitAsync();
|
||||
|
||||
|
@ -29,6 +29,7 @@ public class CipherDetailsQuery : IQuery<CipherDetails>
|
||||
RevisionDate = c.RevisionDate,
|
||||
DeletedDate = c.DeletedDate,
|
||||
Reprompt = c.Reprompt,
|
||||
Key = c.Key,
|
||||
Favorite = _userId.HasValue && c.Favorites != null && c.Favorites.ToLowerInvariant().Contains($"\"{_userId}\":true"),
|
||||
FolderId = (_ignoreFolders || !_userId.HasValue || c.Folders == null || !c.Folders.ToLowerInvariant().Contains(_userId.Value.ToString())) ?
|
||||
null :
|
||||
|
@ -26,6 +26,7 @@ SELECT
|
||||
ELSE TRY_CONVERT(UNIQUEIDENTIFIER, JSON_VALUE(C.[Folders], CONCAT('$."', @UserId, '"')))
|
||||
END [FolderId],
|
||||
C.[DeletedDate],
|
||||
C.[Reprompt]
|
||||
C.[Reprompt],
|
||||
C.[Key]
|
||||
FROM
|
||||
[dbo].[Cipher] C
|
@ -15,7 +15,8 @@
|
||||
@ViewPassword BIT, -- not used
|
||||
@OrganizationUseTotp BIT, -- not used
|
||||
@DeletedDate DATETIME2(7),
|
||||
@Reprompt TINYINT
|
||||
@Reprompt TINYINT,
|
||||
@Key VARCHAR(MAX) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@ -35,7 +36,8 @@ BEGIN
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[DeletedDate],
|
||||
[Reprompt]
|
||||
[Reprompt],
|
||||
[Key]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@ -49,7 +51,8 @@ BEGIN
|
||||
@CreationDate,
|
||||
@RevisionDate,
|
||||
@DeletedDate,
|
||||
@Reprompt
|
||||
@Reprompt,
|
||||
@Key
|
||||
)
|
||||
|
||||
IF @OrganizationId IS NOT NULL
|
||||
|
@ -16,6 +16,7 @@
|
||||
@OrganizationUseTotp BIT, -- not used
|
||||
@DeletedDate DATETIME2(7),
|
||||
@Reprompt TINYINT,
|
||||
@Key VARCHAR(MAX) = NULL,
|
||||
@CollectionIds AS [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
@ -23,7 +24,7 @@ BEGIN
|
||||
|
||||
EXEC [dbo].[CipherDetails_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders,
|
||||
@Attachments, @CreationDate, @RevisionDate, @FolderId, @Favorite, @Edit, @ViewPassword,
|
||||
@OrganizationUseTotp, @DeletedDate, @Reprompt
|
||||
@OrganizationUseTotp, @DeletedDate, @Reprompt, @Key
|
||||
|
||||
DECLARE @UpdateCollectionsSuccess INT
|
||||
EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds
|
||||
|
@ -6,7 +6,7 @@
|
||||
@Data NVARCHAR(MAX),
|
||||
@Favorites NVARCHAR(MAX), -- not used
|
||||
@Folders NVARCHAR(MAX), -- not used
|
||||
@Attachments NVARCHAR(MAX), -- not used
|
||||
@Attachments NVARCHAR(MAX),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@FolderId UNIQUEIDENTIFIER,
|
||||
@ -15,7 +15,8 @@
|
||||
@ViewPassword BIT, -- not used
|
||||
@OrganizationUseTotp BIT, -- not used
|
||||
@DeletedDate DATETIME2(2),
|
||||
@Reprompt TINYINT
|
||||
@Reprompt TINYINT,
|
||||
@Key VARCHAR(MAX) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@ -48,10 +49,12 @@ BEGIN
|
||||
ELSE
|
||||
JSON_MODIFY([Favorites], @UserIdPath, NULL)
|
||||
END,
|
||||
[Attachments] = @Attachments,
|
||||
[Reprompt] = @Reprompt,
|
||||
[CreationDate] = @CreationDate,
|
||||
[RevisionDate] = @RevisionDate,
|
||||
[DeletedDate] = @DeletedDate
|
||||
[DeletedDate] = @DeletedDate,
|
||||
[Key] = @Key
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
|
||||
|
@ -10,7 +10,8 @@
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@DeletedDate DATETIME2(7),
|
||||
@Reprompt TINYINT
|
||||
@Reprompt TINYINT,
|
||||
@Key VARCHAR(MAX) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@ -28,7 +29,8 @@ BEGIN
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[DeletedDate],
|
||||
[Reprompt]
|
||||
[Reprompt],
|
||||
[Key]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@ -43,7 +45,8 @@ BEGIN
|
||||
@CreationDate,
|
||||
@RevisionDate,
|
||||
@DeletedDate,
|
||||
@Reprompt
|
||||
@Reprompt,
|
||||
@Key
|
||||
)
|
||||
|
||||
IF @OrganizationId IS NOT NULL
|
||||
|
@ -11,13 +11,14 @@
|
||||
@RevisionDate DATETIME2(7),
|
||||
@DeletedDate DATETIME2(7),
|
||||
@Reprompt TINYINT,
|
||||
@Key VARCHAR(MAX) = NULL,
|
||||
@CollectionIds AS [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
EXEC [dbo].[Cipher_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders,
|
||||
@Attachments, @CreationDate, @RevisionDate, @DeletedDate, @Reprompt
|
||||
@Attachments, @CreationDate, @RevisionDate, @DeletedDate, @Reprompt, @Key
|
||||
|
||||
DECLARE @UpdateCollectionsSuccess INT
|
||||
EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds
|
||||
|
@ -10,7 +10,8 @@
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@DeletedDate DATETIME2(7),
|
||||
@Reprompt TINYINT
|
||||
@Reprompt TINYINT,
|
||||
@Key VARCHAR(MAX) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@ -28,7 +29,8 @@ BEGIN
|
||||
[CreationDate] = @CreationDate,
|
||||
[RevisionDate] = @RevisionDate,
|
||||
[DeletedDate] = @DeletedDate,
|
||||
[Reprompt] = @Reprompt
|
||||
[Reprompt] = @Reprompt,
|
||||
[Key] = @Key
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
|
||||
|
@ -11,6 +11,7 @@
|
||||
@RevisionDate DATETIME2(7),
|
||||
@DeletedDate DATETIME2(7),
|
||||
@Reprompt TINYINT,
|
||||
@Key VARCHAR(MAX) = NULL,
|
||||
@CollectionIds AS [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
@ -36,7 +37,8 @@ BEGIN
|
||||
[Data] = @Data,
|
||||
[Attachments] = @Attachments,
|
||||
[RevisionDate] = @RevisionDate,
|
||||
[DeletedDate] = @DeletedDate
|
||||
[DeletedDate] = @DeletedDate,
|
||||
[Key] = @Key
|
||||
-- No need to update CreationDate, Favorites, Folders, or Type since that data will not change
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
|
@ -12,6 +12,7 @@ CREATE TABLE [dbo].[Cipher] (
|
||||
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||
[DeletedDate] DATETIME2 (7) NULL,
|
||||
[Reprompt] TINYINT NULL,
|
||||
[Key] VARCHAR(MAX) NULL,
|
||||
CONSTRAINT [PK_Cipher] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||
CONSTRAINT [FK_Cipher_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]),
|
||||
CONSTRAINT [FK_Cipher_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])
|
||||
|
Reference in New Issue
Block a user