From 3f2aa4e9002dc71fafb5b6107862fcdd862a7084 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Tue, 17 Feb 2026 17:38:16 -0500 Subject: [PATCH 1/7] Added new bank account type, added minimum version logic and sync filters --- src/Api/Vault/Controllers/SyncController.cs | 20 ++- .../Vault/Models/CipherBankAccountModel.cs | 63 +++++++++ .../Models/Request/CipherRequestModel.cs | 27 ++++ .../Models/Response/CipherResponseModel.cs | 8 ++ src/Core/Constants.cs | 2 + src/Core/Vault/Enums/CipherType.cs | 1 + .../Models/Data/CipherBankAccountData.cs | 17 +++ .../Services/Implementations/CipherService.cs | 2 + .../Vault/Controllers/SyncControllerTests.cs | 121 ++++++++++++++++++ .../Models/CipherBankAccountModelTests.cs | 56 ++++++++ .../Factories/BankAccountCipherSeeder.cs | 29 +++++ util/Seeder/Models/CipherViewDto.cs | 40 ++++++ util/Seeder/Models/EncryptedCipherDto.cs | 36 ++++++ .../Models/EncryptedCipherDtoExtensions.cs | 17 +++ 14 files changed, 433 insertions(+), 6 deletions(-) create mode 100644 src/Api/Vault/Models/CipherBankAccountModel.cs create mode 100644 src/Core/Vault/Models/Data/CipherBankAccountData.cs create mode 100644 test/Api.Test/Vault/Models/CipherBankAccountModelTests.cs create mode 100644 util/Seeder/Factories/BankAccountCipherSeeder.cs diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index b186e4b60116..21ebc84d4d27 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -42,6 +42,7 @@ public class SyncController : Controller private readonly GlobalSettings _globalSettings; private readonly ICurrentContext _currentContext; private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion); + private readonly Version _bankAccountCipherMinimumVersion = new(Constants.BankAccountCipherMinimumVersion); private readonly IFeatureService _featureService; private readonly IApplicationCacheService _applicationCacheService; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; @@ -104,7 +105,7 @@ await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id, var folders = await _folderRepository.GetManyByUserIdAsync(user.Id); var allCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: hasEnabledOrgs); - var ciphers = FilterSSHKeys(allCiphers); + var ciphers = FilterUnsupportedCipherTypes(allCiphers); var sends = await _sendRepository.GetManyByUserIdAsync(user.Id); IEnumerable collections = null; @@ -141,15 +142,22 @@ await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id, return response; } - private ICollection FilterSSHKeys(ICollection ciphers) + private ICollection FilterUnsupportedCipherTypes(ICollection ciphers) { - if (_currentContext.ClientVersion >= _sshKeyCipherMinimumVersion || _featureService.IsEnabled(FeatureFlagKeys.SSHVersionCheckQAOverride)) + var unsupportedTypes = new List(); + + if (_currentContext.ClientVersion < _sshKeyCipherMinimumVersion && !_featureService.IsEnabled(FeatureFlagKeys.SSHVersionCheckQAOverride)) { - return ciphers; + unsupportedTypes.Add(Core.Vault.Enums.CipherType.SSHKey); } - else + + if (_currentContext.ClientVersion < _bankAccountCipherMinimumVersion && !_featureService.IsEnabled(FeatureFlagKeys.VaultBankAccount)) { - return ciphers.Where(c => c.Type != Core.Vault.Enums.CipherType.SSHKey).ToList(); + unsupportedTypes.Add(Core.Vault.Enums.CipherType.BankAccount); } + + return unsupportedTypes.Count == 0 + ? ciphers + : ciphers.Where(c => !unsupportedTypes.Contains(c.Type)).ToList(); } } diff --git a/src/Api/Vault/Models/CipherBankAccountModel.cs b/src/Api/Vault/Models/CipherBankAccountModel.cs new file mode 100644 index 000000000000..5a461d546230 --- /dev/null +++ b/src/Api/Vault/Models/CipherBankAccountModel.cs @@ -0,0 +1,63 @@ +using Bit.Core.Utilities; +using Bit.Core.Vault.Models.Data; + +namespace Bit.Api.Vault.Models; + +public class CipherBankAccountModel +{ + public CipherBankAccountModel() { } + + public CipherBankAccountModel(CipherBankAccountData data) + { + BankName = data.BankName; + NameOnAccount = data.NameOnAccount; + AccountType = data.AccountType; + AccountNumber = data.AccountNumber; + RoutingNumber = data.RoutingNumber; + BranchNumber = data.BranchNumber; + Pin = data.Pin; + SwiftCode = data.SwiftCode; + Iban = data.Iban; + BankContactPhone = data.BankContactPhone; + } + + [EncryptedString] + [EncryptedStringLength(1000)] + public string? BankName { get; set; } + + [EncryptedString] + [EncryptedStringLength(1000)] + public string? NameOnAccount { get; set; } + + [EncryptedString] + [EncryptedStringLength(1000)] + public string? AccountType { get; set; } + + [EncryptedString] + [EncryptedStringLength(1000)] + public string? AccountNumber { get; set; } + + [EncryptedString] + [EncryptedStringLength(1000)] + public string? RoutingNumber { get; set; } + + [EncryptedString] + [EncryptedStringLength(1000)] + public string? BranchNumber { get; set; } + + [EncryptedString] + [EncryptedStringLength(1000)] + public string? Pin { get; set; } + + [EncryptedString] + [EncryptedStringLength(1000)] + public string? SwiftCode { get; set; } + + [EncryptedString] + [EncryptedStringLength(1000)] + public string? Iban { get; set; } + + [EncryptedString] + [EncryptedStringLength(1000)] + public string? BankContactPhone { get; set; } +} diff --git a/src/Api/Vault/Models/Request/CipherRequestModel.cs b/src/Api/Vault/Models/Request/CipherRequestModel.cs index 9a2c279a28a2..167328329e2f 100644 --- a/src/Api/Vault/Models/Request/CipherRequestModel.cs +++ b/src/Api/Vault/Models/Request/CipherRequestModel.cs @@ -53,6 +53,8 @@ public class CipherRequestModel [Obsolete("Use Data instead.")] public CipherSSHKeyModel SSHKey { get; set; } + [Obsolete("Use Data instead.")] public CipherBankAccountModel BankAccount { get; set; } + /// /// JSON string containing cipher-specific data /// @@ -120,6 +122,10 @@ public Cipher ToCipher(Cipher existingCipher, Guid? userId = null) case CipherType.SSHKey: existingCipher.Data = JsonSerializer.Serialize(ToCipherSSHKeyData(), JsonHelpers.IgnoreWritingNull); break; + case CipherType.BankAccount: + existingCipher.Data = + JsonSerializer.Serialize(ToCipherBankAccountData(), JsonHelpers.IgnoreWritingNull); + break; default: throw new ArgumentException("Unsupported type: " + nameof(Type) + "."); } @@ -296,6 +302,27 @@ private CipherSSHKeyData ToCipherSSHKeyData() }; } + private CipherBankAccountData ToCipherBankAccountData() + { + return new CipherBankAccountData + { + Name = Name, + Notes = Notes, + Fields = Fields?.Select(f => f.ToCipherFieldData()), + PasswordHistory = PasswordHistory?.Select(ph => ph.ToCipherPasswordHistoryData()), + BankName = BankAccount.BankName, + NameOnAccount = BankAccount.NameOnAccount, + AccountType = BankAccount.AccountType, + AccountNumber = BankAccount.AccountNumber, + RoutingNumber = BankAccount.RoutingNumber, + BranchNumber = BankAccount.BranchNumber, + Pin = BankAccount.Pin, + SwiftCode = BankAccount.SwiftCode, + Iban = BankAccount.Iban, + BankContactPhone = BankAccount.BankContactPhone, + }; + } + /// /// Updates a JSON string representing a dictionary by adding, updating, or removing a key-value pair /// based on the provided userIdKey and newValue. diff --git a/src/Api/Vault/Models/Response/CipherResponseModel.cs b/src/Api/Vault/Models/Response/CipherResponseModel.cs index ac11eb3cd302..8b801ca9b9c5 100644 --- a/src/Api/Vault/Models/Response/CipherResponseModel.cs +++ b/src/Api/Vault/Models/Response/CipherResponseModel.cs @@ -54,6 +54,11 @@ public CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bo cipherData = sshKeyData; SSHKey = new CipherSSHKeyModel(sshKeyData); break; + case CipherType.BankAccount: + var bankAccountData = JsonSerializer.Deserialize(cipher.Data); + cipherData = bankAccountData; + BankAccount = new CipherBankAccountModel(bankAccountData); + break; default: throw new ArgumentException("Unsupported " + nameof(Type) + "."); } @@ -98,6 +103,9 @@ public CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bo [Obsolete("Use Data instead.")] public CipherSSHKeyModel SSHKey { get; set; } + [Obsolete("Use Data instead.")] + public CipherBankAccountModel BankAccount { get; set; } + [Obsolete("Use Data instead.")] public IEnumerable Fields { get; set; } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index a0e8482d668b..ab9bc321aec4 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -31,6 +31,7 @@ public static class Constants public const string Fido2KeyCipherMinimumVersion = "2023.10.0"; public const string SSHKeyCipherMinimumVersion = "2024.12.0"; + public const string BankAccountCipherMinimumVersion = "2026.2.0"; public const string DenyLegacyUserMinimumVersion = "2025.6.0"; /// @@ -246,6 +247,7 @@ public static class FeatureFlagKeys public const string SendEmailOTP = "pm-19051-send-email-verification"; /* Vault Team */ + public const string VaultBankAccount = "vault-bank-account"; public const string CipherKeyEncryption = "cipher-key-encryption"; public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk"; public const string PhishingDetection = "phishing-detection"; diff --git a/src/Core/Vault/Enums/CipherType.cs b/src/Core/Vault/Enums/CipherType.cs index a0a49ce990f9..bc8f5f8e4450 100644 --- a/src/Core/Vault/Enums/CipherType.cs +++ b/src/Core/Vault/Enums/CipherType.cs @@ -9,4 +9,5 @@ public enum CipherType : byte Card = 3, Identity = 4, SSHKey = 5, + BankAccount = 6, } diff --git a/src/Core/Vault/Models/Data/CipherBankAccountData.cs b/src/Core/Vault/Models/Data/CipherBankAccountData.cs new file mode 100644 index 000000000000..bf686806b4e2 --- /dev/null +++ b/src/Core/Vault/Models/Data/CipherBankAccountData.cs @@ -0,0 +1,17 @@ +namespace Bit.Core.Vault.Models.Data; + +public class CipherBankAccountData : CipherData +{ + public CipherBankAccountData() { } + + public string? BankName { get; set; } + public string? NameOnAccount { get; set; } + public string? AccountType { get; set; } + public string? AccountNumber { get; set; } + public string? RoutingNumber { get; set; } + public string? BranchNumber { get; set; } + public string? Pin { get; set; } + public string? SwiftCode { get; set; } + public string? Iban { get; set; } + public string? BankContactPhone { get; set; } +} diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index 3a970d82bdff..428e21019d8f 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -1126,6 +1126,7 @@ private string SerializeCipherData(CipherData data) CipherCardData cardData => JsonSerializer.Serialize(cardData), CipherSecureNoteData noteData => JsonSerializer.Serialize(noteData), CipherSSHKeyData sshKeyData => JsonSerializer.Serialize(sshKeyData), + CipherBankAccountData bankAccountData => JsonSerializer.Serialize(bankAccountData), _ => throw new ArgumentException("Unsupported cipher data type.", nameof(data)) }; } @@ -1139,6 +1140,7 @@ private CipherData DeserializeCipherData(Cipher cipher) CipherType.Card => JsonSerializer.Deserialize(cipher.Data), CipherType.SecureNote => JsonSerializer.Deserialize(cipher.Data), CipherType.SSHKey => JsonSerializer.Deserialize(cipher.Data), + CipherType.BankAccount => JsonSerializer.Deserialize(cipher.Data), _ => throw new ArgumentException("Unsupported cipher type.", nameof(cipher)) }; } diff --git a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs index e6d34592c7d1..2aeae359fb32 100644 --- a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs @@ -3,7 +3,9 @@ using AutoFixture; using Bit.Api.Vault.Controllers; using Bit.Api.Vault.Models.Response; +using Bit.Core; using Bit.Core.AdminConsole.Entities; +using Bit.Core.Context; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; @@ -22,6 +24,7 @@ using Bit.Core.Tools.Entities; using Bit.Core.Tools.Repositories; using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; using Bit.Test.Common.AutoFixture; @@ -405,6 +408,124 @@ public async Task Get_HaveMasterPassword_UserDecryptionMasterPasswordUnlockNotNu Assert.Equal(user.Email.ToLower(), result.UserDecryption.MasterPasswordUnlock.Salt); } + [Theory] + [BitAutoData] + public async Task Get_BankAccountCiphers_ReturnedWhenClientVersionSupported( + User user, SutProvider sutProvider) + { + user.EquivalentDomains = null; + user.ExcludedGlobalEquivalentDomains = null; + + var userService = sutProvider.GetDependency(); + userService.GetUserByPrincipalAsync(Arg.Any()).ReturnsForAnyArgs(user); + + var userAccountKeysQuery = sutProvider.GetDependency(); + userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(), + SignatureKeyPairData = null, + }); + + var bankAccountCipher = new CipherDetails { Type = CipherType.BankAccount, Data = "{}", UserId = user.Id }; + var loginCipher = new CipherDetails { Type = CipherType.Login, Data = "{}", UserId = user.Id }; + var ciphers = new List { bankAccountCipher, loginCipher }; + + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id, Arg.Any()).Returns(ciphers); + + sutProvider.GetDependency() + .ClientVersion.Returns(new Version(Constants.BankAccountCipherMinimumVersion)); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(user).Returns(false); + userService.HasPremiumFromOrganization(user).Returns(false); + + var result = await sutProvider.Sut.Get(); + + Assert.Contains(result.Ciphers, c => c.Type == CipherType.BankAccount); + } + + [Theory] + [BitAutoData] + public async Task Get_BankAccountCiphers_FilteredWhenClientVersionTooOld( + User user, SutProvider sutProvider) + { + user.EquivalentDomains = null; + user.ExcludedGlobalEquivalentDomains = null; + + var userService = sutProvider.GetDependency(); + userService.GetUserByPrincipalAsync(Arg.Any()).ReturnsForAnyArgs(user); + + var userAccountKeysQuery = sutProvider.GetDependency(); + userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(), + SignatureKeyPairData = null, + }); + + var bankAccountCipher = new CipherDetails { Type = CipherType.BankAccount, Data = "{}", UserId = user.Id }; + var loginCipher = new CipherDetails { Type = CipherType.Login, Data = "{}", UserId = user.Id }; + var ciphers = new List { bankAccountCipher, loginCipher }; + + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id, Arg.Any()).Returns(ciphers); + + sutProvider.GetDependency() + .ClientVersion.Returns(new Version("2025.1.0")); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.VaultBankAccount).Returns(false); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(user).Returns(false); + userService.HasPremiumFromOrganization(user).Returns(false); + + var result = await sutProvider.Sut.Get(); + + Assert.DoesNotContain(result.Ciphers, c => c.Type == CipherType.BankAccount); + Assert.Contains(result.Ciphers, c => c.Type == CipherType.Login); + } + + [Theory] + [BitAutoData] + public async Task Get_BankAccountCiphers_ReturnedWhenFeatureFlagEnabled( + User user, SutProvider sutProvider) + { + user.EquivalentDomains = null; + user.ExcludedGlobalEquivalentDomains = null; + + var userService = sutProvider.GetDependency(); + userService.GetUserByPrincipalAsync(Arg.Any()).ReturnsForAnyArgs(user); + + var userAccountKeysQuery = sutProvider.GetDependency(); + userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(), + SignatureKeyPairData = null, + }); + + var bankAccountCipher = new CipherDetails { Type = CipherType.BankAccount, Data = "{}", UserId = user.Id }; + var ciphers = new List { bankAccountCipher }; + + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id, Arg.Any()).Returns(ciphers); + + // Old client version but feature flag enabled + sutProvider.GetDependency() + .ClientVersion.Returns(new Version("2025.1.0")); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.VaultBankAccount).Returns(true); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(user).Returns(false); + userService.HasPremiumFromOrganization(user).Returns(false); + + var result = await sutProvider.Sut.Get(); + + Assert.Contains(result.Ciphers, c => c.Type == CipherType.BankAccount); + } + private async Task AssertMethodsCalledAsync(IUserService userService, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IOrganizationUserRepository organizationUserRepository, diff --git a/test/Api.Test/Vault/Models/CipherBankAccountModelTests.cs b/test/Api.Test/Vault/Models/CipherBankAccountModelTests.cs new file mode 100644 index 000000000000..e4d534bc54de --- /dev/null +++ b/test/Api.Test/Vault/Models/CipherBankAccountModelTests.cs @@ -0,0 +1,56 @@ +using Bit.Api.Vault.Models; +using Bit.Core.Vault.Models.Data; +using Xunit; + +namespace Bit.Api.Test.Vault.Models; + +public class CipherBankAccountModelTests +{ + [Fact] + public void Constructor_FromData_MapsAllFields() + { + var data = new CipherBankAccountData + { + BankName = "2.bankName|encrypted", + NameOnAccount = "2.nameOnAccount|encrypted", + AccountType = "2.accountType|encrypted", + AccountNumber = "2.accountNumber|encrypted", + RoutingNumber = "2.routingNumber|encrypted", + BranchNumber = "2.branchNumber|encrypted", + Pin = "2.pin|encrypted", + SwiftCode = "2.swiftCode|encrypted", + Iban = "2.iban|encrypted", + BankContactPhone = "2.bankContactPhone|encrypted", + }; + + var model = new CipherBankAccountModel(data); + + Assert.Equal(data.BankName, model.BankName); + Assert.Equal(data.NameOnAccount, model.NameOnAccount); + Assert.Equal(data.AccountType, model.AccountType); + Assert.Equal(data.AccountNumber, model.AccountNumber); + Assert.Equal(data.RoutingNumber, model.RoutingNumber); + Assert.Equal(data.BranchNumber, model.BranchNumber); + Assert.Equal(data.Pin, model.Pin); + Assert.Equal(data.SwiftCode, model.SwiftCode); + Assert.Equal(data.Iban, model.Iban); + Assert.Equal(data.BankContactPhone, model.BankContactPhone); + } + + [Fact] + public void DefaultConstructor_AllFieldsNull() + { + var model = new CipherBankAccountModel(); + + Assert.Null(model.BankName); + Assert.Null(model.NameOnAccount); + Assert.Null(model.AccountType); + Assert.Null(model.AccountNumber); + Assert.Null(model.RoutingNumber); + Assert.Null(model.BranchNumber); + Assert.Null(model.Pin); + Assert.Null(model.SwiftCode); + Assert.Null(model.Iban); + Assert.Null(model.BankContactPhone); + } +} diff --git a/util/Seeder/Factories/BankAccountCipherSeeder.cs b/util/Seeder/Factories/BankAccountCipherSeeder.cs new file mode 100644 index 000000000000..6544045242c3 --- /dev/null +++ b/util/Seeder/Factories/BankAccountCipherSeeder.cs @@ -0,0 +1,29 @@ +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; +using Bit.Seeder.Models; + +namespace Bit.Seeder.Factories; + +internal static class BankAccountCipherSeeder +{ + internal static Cipher Create( + string encryptionKey, + string name, + BankAccountViewDto bankAccount, + Guid? organizationId = null, + Guid? userId = null, + string? notes = null) + { + var cipherView = new CipherViewDto + { + OrganizationId = organizationId, + Name = name, + Notes = notes, + Type = CipherTypes.BankAccount, + BankAccount = bankAccount + }; + + var encrypted = CipherEncryption.Encrypt(cipherView, encryptionKey); + return CipherEncryption.CreateEntity(encrypted, encrypted.ToBankAccountData(), CipherType.BankAccount, organizationId, userId); + } +} diff --git a/util/Seeder/Models/CipherViewDto.cs b/util/Seeder/Models/CipherViewDto.cs index 0b94d0b0f489..6452b7716806 100644 --- a/util/Seeder/Models/CipherViewDto.cs +++ b/util/Seeder/Models/CipherViewDto.cs @@ -43,6 +43,9 @@ public class CipherViewDto [JsonPropertyName("sshKey")] public SshKeyViewDto? SshKey { get; set; } + [JsonPropertyName("bankAccount")] + public BankAccountViewDto? BankAccount { get; set; } + [JsonPropertyName("favorite")] public bool Favorite { get; set; } @@ -144,6 +147,7 @@ public static class CipherTypes public const int Card = 3; public const int Identity = 4; public const int SshKey = 5; + public const int BankAccount = 6; } public static class RepromptTypes @@ -260,3 +264,39 @@ public record SshKeyViewDto [JsonPropertyName("fingerprint")] public string? Fingerprint { get; init; } } + +/// +/// Bank Account cipher data for SDK encryption. Uses record for composition via `with` expressions. +/// +public record BankAccountViewDto +{ + [JsonPropertyName("bankName")] + public string? BankName { get; init; } + + [JsonPropertyName("nameOnAccount")] + public string? NameOnAccount { get; init; } + + [JsonPropertyName("accountType")] + public string? AccountType { get; init; } + + [JsonPropertyName("accountNumber")] + public string? AccountNumber { get; init; } + + [JsonPropertyName("routingNumber")] + public string? RoutingNumber { get; init; } + + [JsonPropertyName("branchNumber")] + public string? BranchNumber { get; init; } + + [JsonPropertyName("pin")] + public string? Pin { get; init; } + + [JsonPropertyName("swiftCode")] + public string? SwiftCode { get; init; } + + [JsonPropertyName("iban")] + public string? Iban { get; init; } + + [JsonPropertyName("bankContactPhone")] + public string? BankContactPhone { get; init; } +} diff --git a/util/Seeder/Models/EncryptedCipherDto.cs b/util/Seeder/Models/EncryptedCipherDto.cs index f10d7bb4643f..cc12106cfa28 100644 --- a/util/Seeder/Models/EncryptedCipherDto.cs +++ b/util/Seeder/Models/EncryptedCipherDto.cs @@ -37,6 +37,9 @@ public class EncryptedCipherDto [JsonPropertyName("sshKey")] public EncryptedSshKeyDto? SshKey { get; set; } + [JsonPropertyName("bankAccount")] + public EncryptedBankAccountDto? BankAccount { get; set; } + [JsonPropertyName("fields")] public List? Fields { get; set; } @@ -202,3 +205,36 @@ public class EncryptedSshKeyDto [JsonPropertyName("fingerprint")] public string? Fingerprint { get; set; } } + +public class EncryptedBankAccountDto +{ + [JsonPropertyName("bankName")] + public string? BankName { get; set; } + + [JsonPropertyName("nameOnAccount")] + public string? NameOnAccount { get; set; } + + [JsonPropertyName("accountType")] + public string? AccountType { get; set; } + + [JsonPropertyName("accountNumber")] + public string? AccountNumber { get; set; } + + [JsonPropertyName("routingNumber")] + public string? RoutingNumber { get; set; } + + [JsonPropertyName("branchNumber")] + public string? BranchNumber { get; set; } + + [JsonPropertyName("pin")] + public string? Pin { get; set; } + + [JsonPropertyName("swiftCode")] + public string? SwiftCode { get; set; } + + [JsonPropertyName("iban")] + public string? Iban { get; set; } + + [JsonPropertyName("bankContactPhone")] + public string? BankContactPhone { get; set; } +} diff --git a/util/Seeder/Models/EncryptedCipherDtoExtensions.cs b/util/Seeder/Models/EncryptedCipherDtoExtensions.cs index b98fdb08c9ae..d275ff2775ed 100644 --- a/util/Seeder/Models/EncryptedCipherDtoExtensions.cs +++ b/util/Seeder/Models/EncryptedCipherDtoExtensions.cs @@ -79,6 +79,23 @@ internal static class EncryptedCipherDtoExtensions Fields = e.ToFields() }; + internal static CipherBankAccountData ToBankAccountData(this EncryptedCipherDto e) => new() + { + Name = e.Name, + Notes = e.Notes, + BankName = e.BankAccount?.BankName, + NameOnAccount = e.BankAccount?.NameOnAccount, + AccountType = e.BankAccount?.AccountType, + AccountNumber = e.BankAccount?.AccountNumber, + RoutingNumber = e.BankAccount?.RoutingNumber, + BranchNumber = e.BankAccount?.BranchNumber, + Pin = e.BankAccount?.Pin, + SwiftCode = e.BankAccount?.SwiftCode, + Iban = e.BankAccount?.Iban, + BankContactPhone = e.BankAccount?.BankContactPhone, + Fields = e.ToFields() + }; + private static IEnumerable? ToFields(this EncryptedCipherDto e) => e.Fields?.Select(f => new CipherFieldData { From eec1b54ca0d196500e53bca999c1f192544cad21 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Tue, 17 Feb 2026 17:40:30 -0500 Subject: [PATCH 2/7] Formatted changes --- src/Api/Vault/Models/CipherBankAccountModel.cs | 2 +- src/Core/Vault/Models/Data/CipherBankAccountData.cs | 2 +- test/Api.Test/Vault/Controllers/SyncControllerTests.cs | 2 +- test/Api.Test/Vault/Models/CipherBankAccountModelTests.cs | 2 +- util/Seeder/Factories/BankAccountCipherSeeder.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Api/Vault/Models/CipherBankAccountModel.cs b/src/Api/Vault/Models/CipherBankAccountModel.cs index 5a461d546230..4f521692e3d0 100644 --- a/src/Api/Vault/Models/CipherBankAccountModel.cs +++ b/src/Api/Vault/Models/CipherBankAccountModel.cs @@ -1,4 +1,4 @@ -using Bit.Core.Utilities; +using Bit.Core.Utilities; using Bit.Core.Vault.Models.Data; namespace Bit.Api.Vault.Models; diff --git a/src/Core/Vault/Models/Data/CipherBankAccountData.cs b/src/Core/Vault/Models/Data/CipherBankAccountData.cs index bf686806b4e2..fa5b0fb3b2f2 100644 --- a/src/Core/Vault/Models/Data/CipherBankAccountData.cs +++ b/src/Core/Vault/Models/Data/CipherBankAccountData.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Vault.Models.Data; +namespace Bit.Core.Vault.Models.Data; public class CipherBankAccountData : CipherData { diff --git a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs index 2aeae359fb32..b99a8c63eeec 100644 --- a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs @@ -5,12 +5,12 @@ using Bit.Api.Vault.Models.Response; using Bit.Core; using Bit.Core.AdminConsole.Entities; -using Bit.Core.Context; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Models; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/test/Api.Test/Vault/Models/CipherBankAccountModelTests.cs b/test/Api.Test/Vault/Models/CipherBankAccountModelTests.cs index e4d534bc54de..838f19ab724c 100644 --- a/test/Api.Test/Vault/Models/CipherBankAccountModelTests.cs +++ b/test/Api.Test/Vault/Models/CipherBankAccountModelTests.cs @@ -1,4 +1,4 @@ -using Bit.Api.Vault.Models; +using Bit.Api.Vault.Models; using Bit.Core.Vault.Models.Data; using Xunit; diff --git a/util/Seeder/Factories/BankAccountCipherSeeder.cs b/util/Seeder/Factories/BankAccountCipherSeeder.cs index 6544045242c3..a2262ef786e7 100644 --- a/util/Seeder/Factories/BankAccountCipherSeeder.cs +++ b/util/Seeder/Factories/BankAccountCipherSeeder.cs @@ -1,4 +1,4 @@ -using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Entities; using Bit.Core.Vault.Enums; using Bit.Seeder.Models; From 0cc433cc570f043a55e2e8243bd73752d589b8f8 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Tue, 17 Feb 2026 22:48:32 -0500 Subject: [PATCH 3/7] changed condition for filtering bank account types --- src/Api/Vault/Controllers/SyncController.cs | 2 +- src/Core/Constants.cs | 2 +- .../Vault/Controllers/SyncControllerTests.cs | 16 ++++++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index 21ebc84d4d27..dcc6817c0090 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -151,7 +151,7 @@ private ICollection FilterUnsupportedCipherTypes(ICollection diff --git a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs index b99a8c63eeec..3e2bed7e72df 100644 --- a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs @@ -410,7 +410,7 @@ public async Task Get_HaveMasterPassword_UserDecryptionMasterPasswordUnlockNotNu [Theory] [BitAutoData] - public async Task Get_BankAccountCiphers_ReturnedWhenClientVersionSupported( + public async Task Get_BankAccountCiphers_ReturnedWhenFlagEnabledAndClientVersionSupported( User user, SutProvider sutProvider) { user.EquivalentDomains = null; @@ -436,6 +436,9 @@ public async Task Get_BankAccountCiphers_ReturnedWhenClientVersionSupported( sutProvider.GetDependency() .ClientVersion.Returns(new Version(Constants.BankAccountCipherMinimumVersion)); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.VaultBankAccount).Returns(true); + sutProvider.GetDependency() .TwoFactorIsEnabledAsync(user).Returns(false); userService.HasPremiumFromOrganization(user).Returns(false); @@ -447,7 +450,7 @@ public async Task Get_BankAccountCiphers_ReturnedWhenClientVersionSupported( [Theory] [BitAutoData] - public async Task Get_BankAccountCiphers_FilteredWhenClientVersionTooOld( + public async Task Get_BankAccountCiphers_FilteredWhenFlagDisabled( User user, SutProvider sutProvider) { user.EquivalentDomains = null; @@ -470,8 +473,9 @@ public async Task Get_BankAccountCiphers_FilteredWhenClientVersionTooOld( sutProvider.GetDependency() .GetManyByUserIdAsync(user.Id, Arg.Any()).Returns(ciphers); + // New client version but flag disabled sutProvider.GetDependency() - .ClientVersion.Returns(new Version("2025.1.0")); + .ClientVersion.Returns(new Version(Constants.BankAccountCipherMinimumVersion)); sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.VaultBankAccount).Returns(false); @@ -488,7 +492,7 @@ public async Task Get_BankAccountCiphers_FilteredWhenClientVersionTooOld( [Theory] [BitAutoData] - public async Task Get_BankAccountCiphers_ReturnedWhenFeatureFlagEnabled( + public async Task Get_BankAccountCiphers_FilteredWhenFlagEnabledButClientVersionTooOld( User user, SutProvider sutProvider) { user.EquivalentDomains = null; @@ -510,7 +514,7 @@ public async Task Get_BankAccountCiphers_ReturnedWhenFeatureFlagEnabled( sutProvider.GetDependency() .GetManyByUserIdAsync(user.Id, Arg.Any()).Returns(ciphers); - // Old client version but feature flag enabled + // Flag enabled but old client version sutProvider.GetDependency() .ClientVersion.Returns(new Version("2025.1.0")); @@ -523,7 +527,7 @@ public async Task Get_BankAccountCiphers_ReturnedWhenFeatureFlagEnabled( var result = await sutProvider.Sut.Get(); - Assert.Contains(result.Ciphers, c => c.Type == CipherType.BankAccount); + Assert.DoesNotContain(result.Ciphers, c => c.Type == CipherType.BankAccount); } private async Task AssertMethodsCalledAsync(IUserService userService, From 98c5a3077618ee50c1c3f3cd71604f6931573471 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Wed, 18 Feb 2026 13:10:18 -0500 Subject: [PATCH 4/7] Fixed tests --- src/Core/Constants.cs | 2 +- util/Seeder/Models/CipherViewDto.cs | 2 ++ util/Seeder/Models/EncryptedCipherDto.cs | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 361825440697..ab9bc321aec4 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -31,7 +31,7 @@ public static class Constants public const string Fido2KeyCipherMinimumVersion = "2023.10.0"; public const string SSHKeyCipherMinimumVersion = "2024.12.0"; - public const string BankAccountCipherMinimumVersion = "2026.3.0"; + public const string BankAccountCipherMinimumVersion = "2026.2.0"; public const string DenyLegacyUserMinimumVersion = "2025.6.0"; /// diff --git a/util/Seeder/Models/CipherViewDto.cs b/util/Seeder/Models/CipherViewDto.cs index 6452b7716806..f4a4cc8de39d 100644 --- a/util/Seeder/Models/CipherViewDto.cs +++ b/util/Seeder/Models/CipherViewDto.cs @@ -43,7 +43,9 @@ public class CipherViewDto [JsonPropertyName("sshKey")] public SshKeyViewDto? SshKey { get; set; } + // TODO: Remove JsonIgnore condition when Rust SDK supports BankAccount cipher type [JsonPropertyName("bankAccount")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public BankAccountViewDto? BankAccount { get; set; } [JsonPropertyName("favorite")] diff --git a/util/Seeder/Models/EncryptedCipherDto.cs b/util/Seeder/Models/EncryptedCipherDto.cs index cc12106cfa28..20fd93dc38fd 100644 --- a/util/Seeder/Models/EncryptedCipherDto.cs +++ b/util/Seeder/Models/EncryptedCipherDto.cs @@ -37,7 +37,9 @@ public class EncryptedCipherDto [JsonPropertyName("sshKey")] public EncryptedSshKeyDto? SshKey { get; set; } + // TODO: Remove JsonIgnore condition when Rust SDK supports BankAccount cipher type [JsonPropertyName("bankAccount")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public EncryptedBankAccountDto? BankAccount { get; set; } [JsonPropertyName("fields")] From 428aefa7c97348e507273a15b947d6e7b593ff71 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Fri, 27 Feb 2026 18:43:56 -0500 Subject: [PATCH 5/7] Updated feature flag --- src/Api/Vault/Controllers/SyncController.cs | 2 +- src/Core/Constants.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index dcc6817c0090..711206edeaf1 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -151,7 +151,7 @@ private ICollection FilterUnsupportedCipherTypes(ICollection Date: Fri, 27 Feb 2026 18:45:13 -0500 Subject: [PATCH 6/7] Added comment --- src/Core/Constants.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index cd3dc078d20e..5af8f1078f71 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -31,6 +31,8 @@ public static class Constants public const string Fido2KeyCipherMinimumVersion = "2023.10.0"; public const string SSHKeyCipherMinimumVersion = "2024.12.0"; + + // TODO: Update with actual version once the feature is implemented public const string BankAccountCipherMinimumVersion = "2026.2.0"; public const string DenyLegacyUserMinimumVersion = "2025.6.0"; From c4199d3073168746bf92561a4673b8945a82712a Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Fri, 27 Feb 2026 19:12:52 -0500 Subject: [PATCH 7/7] updated feature flag and updated dto --- test/Api.Test/Vault/Controllers/SyncControllerTests.cs | 6 +++--- util/Seeder/Models/CipherViewDto.cs | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs index 3e2bed7e72df..583553674fb5 100644 --- a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs @@ -437,7 +437,7 @@ public async Task Get_BankAccountCiphers_ReturnedWhenFlagEnabledAndClientVersion .ClientVersion.Returns(new Version(Constants.BankAccountCipherMinimumVersion)); sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.VaultBankAccount).Returns(true); + .IsEnabled(FeatureFlagKeys.PM32009_NewItemTypes).Returns(true); sutProvider.GetDependency() .TwoFactorIsEnabledAsync(user).Returns(false); @@ -478,7 +478,7 @@ public async Task Get_BankAccountCiphers_FilteredWhenFlagDisabled( .ClientVersion.Returns(new Version(Constants.BankAccountCipherMinimumVersion)); sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.VaultBankAccount).Returns(false); + .IsEnabled(FeatureFlagKeys.PM32009_NewItemTypes).Returns(false); sutProvider.GetDependency() .TwoFactorIsEnabledAsync(user).Returns(false); @@ -519,7 +519,7 @@ public async Task Get_BankAccountCiphers_FilteredWhenFlagEnabledButClientVersion .ClientVersion.Returns(new Version("2025.1.0")); sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.VaultBankAccount).Returns(true); + .IsEnabled(FeatureFlagKeys.PM32009_NewItemTypes).Returns(true); sutProvider.GetDependency() .TwoFactorIsEnabledAsync(user).Returns(false); diff --git a/util/Seeder/Models/CipherViewDto.cs b/util/Seeder/Models/CipherViewDto.cs index f4a4cc8de39d..6452b7716806 100644 --- a/util/Seeder/Models/CipherViewDto.cs +++ b/util/Seeder/Models/CipherViewDto.cs @@ -43,9 +43,7 @@ public class CipherViewDto [JsonPropertyName("sshKey")] public SshKeyViewDto? SshKey { get; set; } - // TODO: Remove JsonIgnore condition when Rust SDK supports BankAccount cipher type [JsonPropertyName("bankAccount")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public BankAccountViewDto? BankAccount { get; set; } [JsonPropertyName("favorite")]