diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 8bfdf0f0c4..6a7de61f9d 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -27,6 +27,8 @@ namespace Bit.Api.Vault.Controllers; [Authorize("Application")] public class CiphersController : Controller { + private static readonly Version _fido2KeyCipherMinimumVersion = new Version(Constants.Fido2KeyCipherMinimumVersion); + private readonly ICipherRepository _cipherRepository; private readonly ICollectionCipherRepository _collectionCipherRepository; private readonly ICipherService _cipherService; @@ -178,7 +180,8 @@ public class CiphersController : Controller throw new NotFoundException(); } - ValidateItemLevelEncryptionIsAvailable(cipher); + ValidateClientVersionForItemLevelEncryptionSupport(cipher); + ValidateClientVersionForFido2CredentialSupport(cipher); var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id)).Select(c => c.CollectionId).ToList(); var modelOrgId = string.IsNullOrWhiteSpace(model.OrganizationId) ? @@ -202,7 +205,8 @@ public class CiphersController : Controller var userId = _userService.GetProperUserId(User).Value; var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id); - ValidateItemLevelEncryptionIsAvailable(cipher); + ValidateClientVersionForItemLevelEncryptionSupport(cipher); + ValidateClientVersionForFido2CredentialSupport(cipher); if (cipher == null || !cipher.OrganizationId.HasValue || !await _currentContext.EditAnyCollection(cipher.OrganizationId.Value)) @@ -267,6 +271,9 @@ public class CiphersController : Controller throw new NotFoundException(); } + ValidateClientVersionForItemLevelEncryptionSupport(cipher); + ValidateClientVersionForFido2CredentialSupport(cipher); + var original = cipher.Clone(); await _cipherService.ShareAsync(original, model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId), model.CollectionIds.Select(c => new Guid(c)), userId, model.Cipher.LastKnownRevisionDate); @@ -529,7 +536,12 @@ public class CiphersController : Controller throw new BadRequestException("Trying to move ciphers that you do not own."); } - shareCiphers.Add((cipher.ToCipher(ciphersDict[cipher.Id.Value]), cipher.LastKnownRevisionDate)); + var existingCipher = ciphersDict[cipher.Id.Value]; + + ValidateClientVersionForItemLevelEncryptionSupport(existingCipher); + ValidateClientVersionForFido2CredentialSupport(existingCipher); + + shareCiphers.Add((cipher.ToCipher(existingCipher), cipher.LastKnownRevisionDate)); } await _cipherService.ShareManyAsync(shareCiphers, organizationId, @@ -582,7 +594,7 @@ public class CiphersController : Controller throw new NotFoundException(); } - ValidateItemLevelEncryptionIsAvailable(cipher); + ValidateClientVersionForItemLevelEncryptionSupport(cipher); if (request.FileSize > CipherService.MAX_FILE_SIZE) { @@ -804,11 +816,23 @@ public class CiphersController : Controller } } - private void ValidateItemLevelEncryptionIsAvailable(Cipher cipher) + private void ValidateClientVersionForItemLevelEncryptionSupport(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."); } } + + private void ValidateClientVersionForFido2CredentialSupport(Cipher cipher) + { + if (cipher.Type == Core.Vault.Enums.CipherType.Login) + { + var loginData = JsonSerializer.Deserialize(cipher.Data); + if (loginData?.Fido2Credentials != null && _currentContext.ClientVersion < _fido2KeyCipherMinimumVersion) + { + throw new BadRequestException("Cannot edit item. Update to the latest version of Bitwarden and try again."); + } + } + } } diff --git a/src/Api/Vault/Models/CipherFido2CredentialModel.cs b/src/Api/Vault/Models/CipherFido2CredentialModel.cs new file mode 100644 index 0000000000..32f6104a9a --- /dev/null +++ b/src/Api/Vault/Models/CipherFido2CredentialModel.cs @@ -0,0 +1,89 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Utilities; +using Bit.Core.Vault.Models.Data; + +namespace Bit.Api.Vault.Models; + +public class CipherFido2CredentialModel +{ + public CipherFido2CredentialModel() { } + + public CipherFido2CredentialModel(CipherLoginFido2CredentialData data) + { + CredentialId = data.CredentialId; + KeyType = data.KeyType; + KeyAlgorithm = data.KeyAlgorithm; + KeyCurve = data.KeyCurve; + KeyValue = data.KeyValue; + RpId = data.RpId; + RpName = data.RpName; + UserHandle = data.UserHandle; + UserDisplayName = data.UserDisplayName; + Counter = data.Counter; + Discoverable = data.Discoverable; + CreationDate = data.CreationDate; + } + + [EncryptedString] + [EncryptedStringLength(1000)] + public string CredentialId { get; set; } + [EncryptedString] + [EncryptedStringLength(1000)] + public string KeyType { get; set; } + [EncryptedString] + [EncryptedStringLength(1000)] + public string KeyAlgorithm { get; set; } + [EncryptedString] + [EncryptedStringLength(1000)] + public string KeyCurve { get; set; } + [EncryptedString] + [EncryptedStringLength(1000)] + public string KeyValue { get; set; } + [EncryptedString] + [EncryptedStringLength(1000)] + public string RpId { get; set; } + [EncryptedString] + [EncryptedStringLength(1000)] + public string RpName { get; set; } + [EncryptedString] + [EncryptedStringLength(1000)] + public string UserHandle { get; set; } + [EncryptedString] + [EncryptedStringLength(1000)] + public string UserDisplayName { get; set; } + [EncryptedString] + [EncryptedStringLength(1000)] + public string Counter { get; set; } + [EncryptedString] + [EncryptedStringLength(1000)] + public string Discoverable { get; set; } + [Required] + public DateTime CreationDate { get; set; } + + public CipherLoginFido2CredentialData ToCipherLoginFido2CredentialData() + { + return new CipherLoginFido2CredentialData + { + CredentialId = CredentialId, + KeyType = KeyType, + KeyAlgorithm = KeyAlgorithm, + KeyCurve = KeyCurve, + KeyValue = KeyValue, + RpId = RpId, + RpName = RpName, + UserHandle = UserHandle, + UserDisplayName = UserDisplayName, + Counter = Counter, + Discoverable = Discoverable, + CreationDate = CreationDate + }; + } +} + +static class CipherFido2CredentialModelExtensions +{ + public static CipherLoginFido2CredentialData[] ToCipherLoginFido2CredentialData(this CipherFido2CredentialModel[] models) + { + return models.Select(m => m.ToCipherLoginFido2CredentialData()).ToArray(); + } +} diff --git a/src/Api/Vault/Models/CipherLoginModel.cs b/src/Api/Vault/Models/CipherLoginModel.cs index 08bdc082b2..d1ea167513 100644 --- a/src/Api/Vault/Models/CipherLoginModel.cs +++ b/src/Api/Vault/Models/CipherLoginModel.cs @@ -16,6 +16,11 @@ public class CipherLoginModel Uri = data.Uri; } + if (data.Fido2Credentials != null) + { + Fido2Credentials = data.Fido2Credentials.Select(key => new CipherFido2CredentialModel(key)).ToArray(); + } + Username = data.Username; Password = data.Password; PasswordRevisionDate = data.PasswordRevisionDate; @@ -55,6 +60,7 @@ public class CipherLoginModel [EncryptedStringLength(1000)] public string Totp { get; set; } public bool? AutofillOnPageLoad { get; set; } + public CipherFido2CredentialModel[] Fido2Credentials { get; set; } public class CipherLoginUriModel { diff --git a/src/Api/Vault/Models/Request/CipherRequestModel.cs b/src/Api/Vault/Models/Request/CipherRequestModel.cs index 7de10878b8..b62f2ff96e 100644 --- a/src/Api/Vault/Models/Request/CipherRequestModel.cs +++ b/src/Api/Vault/Models/Request/CipherRequestModel.cs @@ -166,6 +166,7 @@ public class CipherRequestModel PasswordRevisionDate = Login.PasswordRevisionDate, Totp = Login.Totp, AutofillOnPageLoad = Login.AutofillOnPageLoad, + Fido2Credentials = Login.Fido2Credentials == null ? null : Login.Fido2Credentials.ToCipherLoginFido2CredentialData(), }; } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 8dea0561f9..e85eecb36e 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -20,6 +20,8 @@ public static class Constants /// public const int OrganizationSelfHostSubscriptionGracePeriodDays = 60; + public const string Fido2KeyCipherMinimumVersion = "2023.10.0"; + public const string CipherKeyEncryptionMinimumVersion = "2023.9.2"; } @@ -38,6 +40,7 @@ public static class FeatureFlagKeys public const string DisplayEuEnvironment = "display-eu-environment"; public const string DisplayLowKdfIterationWarning = "display-kdf-iteration-warning"; public const string TrustedDeviceEncryption = "trusted-device-encryption"; + public const string Fido2VaultCredentials = "fido2-vault-credentials"; public const string AutofillV2 = "autofill-v2"; public const string BrowserFilelessImport = "browser-fileless-import"; @@ -54,7 +57,8 @@ public static class FeatureFlagKeys // place overriding values when needed locally (offline), or return null return new Dictionary() { - { TrustedDeviceEncryption, "true" } + { TrustedDeviceEncryption, "true" }, + { Fido2VaultCredentials, "true" } }; } } diff --git a/src/Core/Vault/Enums/CipherType.cs b/src/Core/Vault/Enums/CipherType.cs index c5a61043d3..f3c7a90f45 100644 --- a/src/Core/Vault/Enums/CipherType.cs +++ b/src/Core/Vault/Enums/CipherType.cs @@ -7,5 +7,5 @@ public enum CipherType : byte Login = 1, SecureNote = 2, Card = 3, - Identity = 4 + Identity = 4, } diff --git a/src/Core/Vault/Models/Data/CipherLoginData.cs b/src/Core/Vault/Models/Data/CipherLoginData.cs index 223f85ef2b..e952b39cf2 100644 --- a/src/Core/Vault/Models/Data/CipherLoginData.cs +++ b/src/Core/Vault/Models/Data/CipherLoginData.cs @@ -19,6 +19,7 @@ public class CipherLoginData : CipherData public DateTime? PasswordRevisionDate { get; set; } public string Totp { get; set; } public bool? AutofillOnPageLoad { get; set; } + public CipherLoginFido2CredentialData[] Fido2Credentials { get; set; } public class CipherLoginUriData { diff --git a/src/Core/Vault/Models/Data/CipherLoginFido2CredentialData.cs b/src/Core/Vault/Models/Data/CipherLoginFido2CredentialData.cs new file mode 100644 index 0000000000..c0801d2a6c --- /dev/null +++ b/src/Core/Vault/Models/Data/CipherLoginFido2CredentialData.cs @@ -0,0 +1,19 @@ +namespace Bit.Core.Vault.Models.Data; + +public class CipherLoginFido2CredentialData +{ + public CipherLoginFido2CredentialData() { } + + public string CredentialId { get; set; } + public string KeyType { get; set; } + public string KeyAlgorithm { get; set; } + public string KeyCurve { get; set; } + public string KeyValue { get; set; } + public string RpId { get; set; } + public string RpName { get; set; } + public string UserHandle { get; set; } + public string UserDisplayName { get; set; } + public string Counter { get; set; } + public string Discoverable { get; set; } + public DateTime CreationDate { get; set; } +}