From 8c77c65ce8d4673b46f0fd4946c0741b6ffc8faf Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Tue, 17 Oct 2023 18:17:13 +0200 Subject: [PATCH] [PM-1222] Passkeys in the Bitwarden vault (#2679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [EC-598] feat: add support for saving fido2 keys * [EC-598] feat: add additional data * [EC-598] feat: add counter, nonDiscoverableId; remove origin * [EC-598] fix: previous incomplete commit * [EC-598] fix: previous incomplete commit.. again * [EC-598] fix: failed merge * [EC-598] fix: move files around to match new structure * [EC-598] feat: add implementation for non-discoverable credentials * [EC-598] chore: remove some changes introduced by vs * [EC-598] fix: linting issues * [PM-1500] Add feature flag to enable pass keys (#2916) * Added feature flag to enable pass keys * Renamed enable pass keys to fido2 vault credentials * only sync fido2key ciphers on clients >=2023.9.0 (#3244) * Renamed fido2key property username to userDisplayName (#3172) * [PM-1859] Renamed NonDiscoverableId to credentialId (#3198) * PM-1859 Refactor to credentialId * PM-1859 Removed unnecessary import --------- Co-authored-by: Andreas Coroiu * [PM-3807] Store all passkeys as login cipher type (#3261) * [PM-3807] feat: add discoverable property to fido2key * [PM-3807] feat: remove standalone Fido2Key * [PM-3807] chore: clean up unusued constant * [PM-3807] fix: remove standadlone Fido2Key property that I missed * [PM-3807] Store passkeys in array (#3268) * [PM-3807] feat: store passkeys in array * [PM-3807] amazing adventures with the c# linter * [PM-3980] Added creationDate property to the Fido2Key object (#3279) * Added creationDate property to the Fido2Key object * Fixed lint issues * fixed comments * made createionDate required * [PM-3808] [Storage v2] Add old client/new server backward compatibility (#3262) * [PM-3807] feat: add discoverable property to fido2key * [PM-3807] feat: remove standalone Fido2Key * [PM-3807] chore: clean up unusued constant * [PM-3808] feat: add fido2 compatibility check before saving ciphers * Resolved merge conflicts. * Setting minimum version for QA. --------- Co-authored-by: Todd Martin * [PM-4054] Rename Fido2Key to Fido2Credential (#3306) * Add server version compatibility check for Fido2Credentials on sharing with org (#3328) * Added compatibility checks. * Refactored into separate methods for easier removal. * Added check on ShareMany * Updated method order to be consistent. * Linting * Updated minimum server version for release, as well as defaulting the feature on for self-hosted. * Added trailing space. * Removed extra assignment --------- Co-authored-by: gbubemismith Co-authored-by: SmithThe4th Co-authored-by: Todd Martin Co-authored-by: Kyle Spearrin Co-authored-by: Carlos Gonçalves Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com> Co-authored-by: Oscar Hinton --- .../Vault/Controllers/CiphersController.cs | 34 +++++-- .../Models/CipherFido2CredentialModel.cs | 89 +++++++++++++++++++ src/Api/Vault/Models/CipherLoginModel.cs | 6 ++ .../Models/Request/CipherRequestModel.cs | 1 + src/Core/Constants.cs | 6 +- src/Core/Vault/Enums/CipherType.cs | 2 +- src/Core/Vault/Models/Data/CipherLoginData.cs | 1 + .../Data/CipherLoginFido2CredentialData.cs | 19 ++++ 8 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 src/Api/Vault/Models/CipherFido2CredentialModel.cs create mode 100644 src/Core/Vault/Models/Data/CipherLoginFido2CredentialData.cs 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; } +}