diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index 0381bdca6c48..f73f5c5c135b 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -1,7 +1,9 @@ using Bit.Api.Vault.Models.Response; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -10,6 +12,7 @@ using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tools.Repositories; +using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -30,6 +33,8 @@ public class SyncController : Controller private readonly IPolicyRepository _policyRepository; private readonly ISendRepository _sendRepository; private readonly GlobalSettings _globalSettings; + private readonly ICurrentContext _currentContext; + private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion); public SyncController( IUserService userService, @@ -41,7 +46,8 @@ public SyncController( IProviderUserRepository providerUserRepository, IPolicyRepository policyRepository, ISendRepository sendRepository, - GlobalSettings globalSettings) + GlobalSettings globalSettings, + ICurrentContext currentContext) { _userService = userService; _folderRepository = folderRepository; @@ -53,6 +59,7 @@ public SyncController( _policyRepository = policyRepository; _sendRepository = sendRepository; _globalSettings = globalSettings; + _currentContext = currentContext; } [HttpGet("")] @@ -74,7 +81,8 @@ await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id, var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled); var folders = await _folderRepository.GetManyByUserIdAsync(user.Id); - var ciphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: hasEnabledOrgs); + var allCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: hasEnabledOrgs); + var ciphers = FilterSSHKeys(allCiphers); var sends = await _sendRepository.GetManyByUserIdAsync(user.Id); IEnumerable collections = null; @@ -95,4 +103,16 @@ await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id, collectionCiphersGroupDict, excludeDomains, policies, sends); return response; } + + private ICollection FilterSSHKeys(ICollection ciphers) + { + if (_currentContext.ClientVersion >= _sshKeyCipherMinimumVersion && _currentContext.DeviceType != DeviceType.Android && _currentContext.DeviceType != DeviceType.iOS && _currentContext.DeviceType != DeviceType.AndroidAmazon) + { + return ciphers; + } + else + { + return ciphers.Where(c => c.Type != Core.Vault.Enums.CipherType.SSHKey).ToList(); + } + } } diff --git a/src/Api/Vault/Models/CipherSSHKeyModel.cs b/src/Api/Vault/Models/CipherSSHKeyModel.cs new file mode 100644 index 000000000000..47853aa36e0f --- /dev/null +++ b/src/Api/Vault/Models/CipherSSHKeyModel.cs @@ -0,0 +1,26 @@ +using Bit.Core.Utilities; +using Bit.Core.Vault.Models.Data; + +namespace Bit.Api.Vault.Models; + +public class CipherSSHKeyModel +{ + public CipherSSHKeyModel() { } + + public CipherSSHKeyModel(CipherSSHKeyData data) + { + PrivateKey = data.PrivateKey; + PublicKey = data.PublicKey; + KeyFingerprint = data.KeyFingerprint; + } + + [EncryptedString] + [EncryptedStringLength(5000)] + public string PrivateKey { get; set; } + [EncryptedString] + [EncryptedStringLength(5000)] + public string PublicKey { get; set; } + [EncryptedString] + [EncryptedStringLength(1000)] + public string KeyFingerprint { get; set; } +} diff --git a/src/Api/Vault/Models/Request/CipherRequestModel.cs b/src/Api/Vault/Models/Request/CipherRequestModel.cs index b62f2ff96e35..89eda415b1af 100644 --- a/src/Api/Vault/Models/Request/CipherRequestModel.cs +++ b/src/Api/Vault/Models/Request/CipherRequestModel.cs @@ -37,6 +37,7 @@ public class CipherRequestModel public CipherCardModel Card { get; set; } public CipherIdentityModel Identity { get; set; } public CipherSecureNoteModel SecureNote { get; set; } + public CipherSSHKeyModel SSHKey { get; set; } public DateTime? LastKnownRevisionDate { get; set; } = null; public CipherDetails ToCipherDetails(Guid userId, bool allowOrgIdSet = true) @@ -82,6 +83,9 @@ public Cipher ToCipher(Cipher existingCipher) case CipherType.SecureNote: existingCipher.Data = JsonSerializer.Serialize(ToCipherSecureNoteData(), JsonHelpers.IgnoreWritingNull); break; + case CipherType.SSHKey: + existingCipher.Data = JsonSerializer.Serialize(ToCipherSSHKeyData(), JsonHelpers.IgnoreWritingNull); + break; default: throw new ArgumentException("Unsupported type: " + nameof(Type) + "."); } @@ -230,6 +234,21 @@ private CipherSecureNoteData ToCipherSecureNoteData() Type = SecureNote.Type, }; } + + private CipherSSHKeyData ToCipherSSHKeyData() + { + return new CipherSSHKeyData + { + Name = Name, + Notes = Notes, + Fields = Fields?.Select(f => f.ToCipherFieldData()), + PasswordHistory = PasswordHistory?.Select(ph => ph.ToCipherPasswordHistoryData()), + + PrivateKey = SSHKey.PrivateKey, + PublicKey = SSHKey.PublicKey, + KeyFingerprint = SSHKey.KeyFingerprint, + }; + } } public class CipherWithIdRequestModel : CipherRequestModel diff --git a/src/Api/Vault/Models/Response/CipherResponseModel.cs b/src/Api/Vault/Models/Response/CipherResponseModel.cs index aa86b17f5258..10b77274b50a 100644 --- a/src/Api/Vault/Models/Response/CipherResponseModel.cs +++ b/src/Api/Vault/Models/Response/CipherResponseModel.cs @@ -48,6 +48,12 @@ public CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bo cipherData = identityData; Identity = new CipherIdentityModel(identityData); break; + case CipherType.SSHKey: + var sshKeyData = JsonSerializer.Deserialize(cipher.Data); + Data = sshKeyData; + cipherData = sshKeyData; + SSHKey = new CipherSSHKeyModel(sshKeyData); + break; default: throw new ArgumentException("Unsupported " + nameof(Type) + "."); } @@ -76,6 +82,7 @@ public CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bo public CipherCardModel Card { get; set; } public CipherIdentityModel Identity { get; set; } public CipherSecureNoteModel SecureNote { get; set; } + public CipherSSHKeyModel SSHKey { get; set; } public IEnumerable Fields { get; set; } public IEnumerable PasswordHistory { get; set; } public IEnumerable Attachments { get; set; } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 34f499c96ee6..ba543bd79b00 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -22,6 +22,7 @@ public static class Constants public const int OrganizationSelfHostSubscriptionGracePeriodDays = 60; public const string Fido2KeyCipherMinimumVersion = "2023.10.0"; + public const string SSHKeyCipherMinimumVersion = "2024.7.0"; /// /// Used by IdentityServer to identify our own provider. @@ -131,6 +132,7 @@ public static class FeatureFlagKeys public const string AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page"; public const string ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner"; public const string DeviceTrustLogging = "pm-8285-device-trust-logging"; + public const string SSHKeyItemVaultItem = "ssh-key-vault-item"; public static List GetAllKeys() { diff --git a/src/Core/Vault/Enums/CipherType.cs b/src/Core/Vault/Enums/CipherType.cs index f3c7a90f454e..a0a49ce990f9 100644 --- a/src/Core/Vault/Enums/CipherType.cs +++ b/src/Core/Vault/Enums/CipherType.cs @@ -8,4 +8,5 @@ public enum CipherType : byte SecureNote = 2, Card = 3, Identity = 4, + SSHKey = 5, } diff --git a/src/Core/Vault/Models/Data/CipherSSHKeyData.cs b/src/Core/Vault/Models/Data/CipherSSHKeyData.cs new file mode 100644 index 000000000000..45c2cf607498 --- /dev/null +++ b/src/Core/Vault/Models/Data/CipherSSHKeyData.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Vault.Models.Data; + +public class CipherSSHKeyData : CipherData +{ + public CipherSSHKeyData() { } + + public string PrivateKey { get; set; } + public string PublicKey { get; set; } + public string KeyFingerprint { get; set; } +}