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
2 changes: 1 addition & 1 deletion src/Api/Vault/Controllers/CiphersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1645,7 +1645,7 @@ private void ValidateAttachment()

private void ValidateClientVersionForFido2CredentialSupport(Cipher cipher)
{
if (cipher.Type == Core.Vault.Enums.CipherType.Login)
if (cipher.Type == Core.Vault.Enums.CipherType.Login && !cipher.IsDataBlobEncrypted())
{
var loginData = JsonSerializer.Deserialize<CipherLoginData>(cipher.Data);
if (loginData?.Fido2Credentials != null && _currentContext.ClientVersion < _fido2KeyCipherMinimumVersion)
Expand Down
21 changes: 13 additions & 8 deletions src/Api/Vault/Models/Response/CipherResponseModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ public CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bo
Id = cipher.Id;
Type = cipher.Type;
Data = cipher.Data;
RevisionDate = cipher.RevisionDate;
OrganizationId = cipher.OrganizationId;
Attachments = AttachmentResponseModel.FromCipher(cipher, globalSettings);
OrganizationUseTotp = orgUseTotp;
CreationDate = cipher.CreationDate;
DeletedDate = cipher.DeletedDate;
Reprompt = cipher.Reprompt.GetValueOrDefault(CipherRepromptType.None);
Key = cipher.Key;

if (cipher.IsDataBlobEncrypted())
{
return;
}

CipherData cipherData;
switch (cipher.Type)
Expand Down Expand Up @@ -78,14 +91,6 @@ public CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bo
Notes = cipherData.Notes;
Fields = cipherData.Fields?.Select(f => new CipherFieldModel(f));
PasswordHistory = cipherData.PasswordHistory?.Select(ph => new CipherPasswordHistoryModel(ph));
RevisionDate = cipher.RevisionDate;
OrganizationId = cipher.OrganizationId;
Attachments = AttachmentResponseModel.FromCipher(cipher, globalSettings);
OrganizationUseTotp = orgUseTotp;
CreationDate = cipher.CreationDate;
DeletedDate = cipher.DeletedDate;
Reprompt = cipher.Reprompt.GetValueOrDefault(CipherRepromptType.None);
Key = cipher.Key;
}

public Guid Id { get; set; }
Expand Down
11 changes: 11 additions & 0 deletions src/Core/Vault/Entities/Cipher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ public void SetNewId()
Id = CoreHelpers.GenerateComb();
}

public bool IsDataBlobEncrypted()
{
if (string.IsNullOrWhiteSpace(Data))
{
return false;
}

var span = Data.AsSpan().TrimStart();
return span.Length > 0 && span[0] != '{';
}

public Dictionary<string, CipherAttachment.MetaData> GetAttachments()
{
if (string.IsNullOrWhiteSpace(Attachments))
Expand Down
43 changes: 43 additions & 0 deletions test/Api.Test/Vault/Controllers/CiphersControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,49 @@ public async Task PutPartialShouldReturnCipherWithGivenFolderAndFavoriteValues(U
Assert.Equal(isFavorite, result.Favorite);
}

[Theory, BitAutoData]
public async Task Put_OpaqueLoginCipherWithOldClient_SkipsFido2VersionCheck(
User user,
SutProvider<CiphersController> sutProvider)
{
var cipherId = Guid.NewGuid();
var cipherDetails = new CipherDetails
{
Id = cipherId,
UserId = user.Id,
Type = CipherType.Login,
Data = "2.iv|ct|mac",
Edit = true,
ViewPassword = true,
};

sutProvider.GetDependency<IUserService>()
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(user);
sutProvider.GetDependency<ICipherRepository>()
.GetByIdAsync(cipherId, user.Id)
.Returns(cipherDetails);
sutProvider.GetDependency<ICurrentContext>()
.ClientVersion
.Returns(new Version(2022, 1, 0));

var model = new CipherRequestModel
{
Type = CipherType.Login,
Name = "2.name|encrypted",
Data = "2.iv|ct|mac",
};

var response = await sutProvider.Sut.Put(cipherId, model);

Assert.NotNull(response);
Assert.Equal("2.iv|ct|mac", response.Data);
Assert.Null(response.Login);
await sutProvider.GetDependency<ICipherService>()
.Received(1)
.SaveDetailsAsync(Arg.Any<CipherDetails>(), user.Id, Arg.Any<DateTime?>(), Arg.Any<IEnumerable<Guid>>());
}

[Theory, BitAutoData]
public async Task PutPartialShouldThrowNotFoundExceptionWhenCipherDoesNotExist(User user, Guid folderId, SutProvider<CiphersController> sutProvider)
{
Expand Down
39 changes: 39 additions & 0 deletions test/Api.Test/Vault/Models/Response/CipherResponseModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,4 +269,43 @@ public void Constructor_Passport_PreservesRawDataField()

Assert.Equal(serializedData, response.Data);
}

[Theory]
[InlineData(CipherType.Login)]
[InlineData(CipherType.SecureNote)]
[InlineData(CipherType.Card)]
[InlineData(CipherType.Identity)]
[InlineData(CipherType.SSHKey)]
[InlineData(CipherType.BankAccount)]
[InlineData(CipherType.DriversLicense)]
[InlineData(CipherType.Passport)]
public void Constructor_OpaqueData_DoesNotThrowAndSkipsLegacyFields(CipherType type)
{
const string opaque = "2.iv|ct|mac";
var cipher = new Cipher
{
Id = Guid.NewGuid(),
Type = type,
Data = opaque,
RevisionDate = DateTime.UtcNow,
CreationDate = DateTime.UtcNow,
};

var response = new CipherMiniResponseModel(cipher, _globalSettings, false);

Assert.Equal(type, response.Type);
Assert.Equal(opaque, response.Data);
Assert.Null(response.Name);
Assert.Null(response.Notes);
Assert.Null(response.Login);
Assert.Null(response.SecureNote);
Assert.Null(response.Card);
Assert.Null(response.Identity);
Assert.Null(response.SSHKey);
Assert.Null(response.BankAccount);
Assert.Null(response.DriversLicense);
Assert.Null(response.Passport);
Assert.Null(response.Fields);
Assert.Null(response.PasswordHistory);
}
}
22 changes: 22 additions & 0 deletions test/Core.Test/Vault/Models/CipherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,26 @@ public void Clone_OrganizationCipher_CreatesExactCopy(Cipher cipher)
{
Assert.Equal(JsonSerializer.Serialize(cipher), JsonSerializer.Serialize(cipher.Clone()));
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("{\"Name\":\"x\"}")]
[InlineData(" { \"Name\": \"x\" }")]
public void IsDataBlobEncrypted_LegacyOrEmpty_ReturnsFalse(string? data)
{
var cipher = new Cipher { Data = data! };
Assert.False(cipher.IsDataBlobEncrypted());
}

[Theory]
[InlineData("2.iv|ct|mac")]
[InlineData("plain string")]
[InlineData(" 2.iv|ct|mac")]
public void IsDataBlobEncrypted_OpaqueData_ReturnsTrue(string data)
{
var cipher = new Cipher { Data = data };
Assert.True(cipher.IsDataBlobEncrypted());
}
}
Loading