mirror of
https://github.com/bitwarden/server.git
synced 2025-04-06 21:48:12 -05:00
[PM-1222] Passkeys in the Bitwarden vault (#2679)
* [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 <andreas.coroiu@gmail.com> * [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 <tmartin@bitwarden.com> * [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 <gsmithwalter@gmail.com> Co-authored-by: SmithThe4th <gsmith@bitwarden.com> Co-authored-by: Todd Martin <tmartin@bitwarden.com> Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com> Co-authored-by: Carlos Gonçalves <carlosmaccam@gmail.com> Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com> Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
This commit is contained in:
parent
8177821e8b
commit
8c77c65ce8
@ -27,6 +27,8 @@ namespace Bit.Api.Vault.Controllers;
|
|||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
public class CiphersController : Controller
|
public class CiphersController : Controller
|
||||||
{
|
{
|
||||||
|
private static readonly Version _fido2KeyCipherMinimumVersion = new Version(Constants.Fido2KeyCipherMinimumVersion);
|
||||||
|
|
||||||
private readonly ICipherRepository _cipherRepository;
|
private readonly ICipherRepository _cipherRepository;
|
||||||
private readonly ICollectionCipherRepository _collectionCipherRepository;
|
private readonly ICollectionCipherRepository _collectionCipherRepository;
|
||||||
private readonly ICipherService _cipherService;
|
private readonly ICipherService _cipherService;
|
||||||
@ -178,7 +180,8 @@ public class CiphersController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateItemLevelEncryptionIsAvailable(cipher);
|
ValidateClientVersionForItemLevelEncryptionSupport(cipher);
|
||||||
|
ValidateClientVersionForFido2CredentialSupport(cipher);
|
||||||
|
|
||||||
var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id)).Select(c => c.CollectionId).ToList();
|
var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id)).Select(c => c.CollectionId).ToList();
|
||||||
var modelOrgId = string.IsNullOrWhiteSpace(model.OrganizationId) ?
|
var modelOrgId = string.IsNullOrWhiteSpace(model.OrganizationId) ?
|
||||||
@ -202,7 +205,8 @@ public class CiphersController : Controller
|
|||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id);
|
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id);
|
||||||
|
|
||||||
ValidateItemLevelEncryptionIsAvailable(cipher);
|
ValidateClientVersionForItemLevelEncryptionSupport(cipher);
|
||||||
|
ValidateClientVersionForFido2CredentialSupport(cipher);
|
||||||
|
|
||||||
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
||||||
!await _currentContext.EditAnyCollection(cipher.OrganizationId.Value))
|
!await _currentContext.EditAnyCollection(cipher.OrganizationId.Value))
|
||||||
@ -267,6 +271,9 @@ public class CiphersController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ValidateClientVersionForItemLevelEncryptionSupport(cipher);
|
||||||
|
ValidateClientVersionForFido2CredentialSupport(cipher);
|
||||||
|
|
||||||
var original = cipher.Clone();
|
var original = cipher.Clone();
|
||||||
await _cipherService.ShareAsync(original, model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId),
|
await _cipherService.ShareAsync(original, model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId),
|
||||||
model.CollectionIds.Select(c => new Guid(c)), userId, model.Cipher.LastKnownRevisionDate);
|
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.");
|
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,
|
await _cipherService.ShareManyAsync(shareCiphers, organizationId,
|
||||||
@ -582,7 +594,7 @@ public class CiphersController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateItemLevelEncryptionIsAvailable(cipher);
|
ValidateClientVersionForItemLevelEncryptionSupport(cipher);
|
||||||
|
|
||||||
if (request.FileSize > CipherService.MAX_FILE_SIZE)
|
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)
|
if (cipher.Key != null && _currentContext.ClientVersion < _cipherKeyEncryptionMinimumVersion)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Cannot edit item. Update to the latest version of Bitwarden and try again.");
|
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<CipherLoginData>(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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
89
src/Api/Vault/Models/CipherFido2CredentialModel.cs
Normal file
89
src/Api/Vault/Models/CipherFido2CredentialModel.cs
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,11 @@ public class CipherLoginModel
|
|||||||
Uri = data.Uri;
|
Uri = data.Uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.Fido2Credentials != null)
|
||||||
|
{
|
||||||
|
Fido2Credentials = data.Fido2Credentials.Select(key => new CipherFido2CredentialModel(key)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
Username = data.Username;
|
Username = data.Username;
|
||||||
Password = data.Password;
|
Password = data.Password;
|
||||||
PasswordRevisionDate = data.PasswordRevisionDate;
|
PasswordRevisionDate = data.PasswordRevisionDate;
|
||||||
@ -55,6 +60,7 @@ public class CipherLoginModel
|
|||||||
[EncryptedStringLength(1000)]
|
[EncryptedStringLength(1000)]
|
||||||
public string Totp { get; set; }
|
public string Totp { get; set; }
|
||||||
public bool? AutofillOnPageLoad { get; set; }
|
public bool? AutofillOnPageLoad { get; set; }
|
||||||
|
public CipherFido2CredentialModel[] Fido2Credentials { get; set; }
|
||||||
|
|
||||||
public class CipherLoginUriModel
|
public class CipherLoginUriModel
|
||||||
{
|
{
|
||||||
|
@ -166,6 +166,7 @@ public class CipherRequestModel
|
|||||||
PasswordRevisionDate = Login.PasswordRevisionDate,
|
PasswordRevisionDate = Login.PasswordRevisionDate,
|
||||||
Totp = Login.Totp,
|
Totp = Login.Totp,
|
||||||
AutofillOnPageLoad = Login.AutofillOnPageLoad,
|
AutofillOnPageLoad = Login.AutofillOnPageLoad,
|
||||||
|
Fido2Credentials = Login.Fido2Credentials == null ? null : Login.Fido2Credentials.ToCipherLoginFido2CredentialData(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,8 @@ public static class Constants
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const int OrganizationSelfHostSubscriptionGracePeriodDays = 60;
|
public const int OrganizationSelfHostSubscriptionGracePeriodDays = 60;
|
||||||
|
|
||||||
|
public const string Fido2KeyCipherMinimumVersion = "2023.10.0";
|
||||||
|
|
||||||
public const string CipherKeyEncryptionMinimumVersion = "2023.9.2";
|
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 DisplayEuEnvironment = "display-eu-environment";
|
||||||
public const string DisplayLowKdfIterationWarning = "display-kdf-iteration-warning";
|
public const string DisplayLowKdfIterationWarning = "display-kdf-iteration-warning";
|
||||||
public const string TrustedDeviceEncryption = "trusted-device-encryption";
|
public const string TrustedDeviceEncryption = "trusted-device-encryption";
|
||||||
|
public const string Fido2VaultCredentials = "fido2-vault-credentials";
|
||||||
public const string AutofillV2 = "autofill-v2";
|
public const string AutofillV2 = "autofill-v2";
|
||||||
public const string BrowserFilelessImport = "browser-fileless-import";
|
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
|
// place overriding values when needed locally (offline), or return null
|
||||||
return new Dictionary<string, string>()
|
return new Dictionary<string, string>()
|
||||||
{
|
{
|
||||||
{ TrustedDeviceEncryption, "true" }
|
{ TrustedDeviceEncryption, "true" },
|
||||||
|
{ Fido2VaultCredentials, "true" }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,5 +7,5 @@ public enum CipherType : byte
|
|||||||
Login = 1,
|
Login = 1,
|
||||||
SecureNote = 2,
|
SecureNote = 2,
|
||||||
Card = 3,
|
Card = 3,
|
||||||
Identity = 4
|
Identity = 4,
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ public class CipherLoginData : CipherData
|
|||||||
public DateTime? PasswordRevisionDate { get; set; }
|
public DateTime? PasswordRevisionDate { get; set; }
|
||||||
public string Totp { get; set; }
|
public string Totp { get; set; }
|
||||||
public bool? AutofillOnPageLoad { get; set; }
|
public bool? AutofillOnPageLoad { get; set; }
|
||||||
|
public CipherLoginFido2CredentialData[] Fido2Credentials { get; set; }
|
||||||
|
|
||||||
public class CipherLoginUriData
|
public class CipherLoginUriData
|
||||||
{
|
{
|
||||||
|
19
src/Core/Vault/Models/Data/CipherLoginFido2CredentialData.cs
Normal file
19
src/Core/Vault/Models/Data/CipherLoginFido2CredentialData.cs
Normal file
@ -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; }
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user