diff --git a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift b/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift index f6c6945f7..5980d0aff 100644 --- a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift +++ b/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift @@ -24,6 +24,8 @@ enum FeatureFlag: String, CaseIterable, Codable { /// A feature flag for the create account flow. case nativeCreateAccountFlow = "native-create-account-flow" + case sshKeyVaultItem = "ssh-key-vault-item" + // MARK: Test Flags /// A test feature flag that isn't remotely configured and has no initial value. @@ -81,6 +83,7 @@ enum FeatureFlag: String, CaseIterable, Codable { .importLoginsFlow, .nativeCarouselFlow, .nativeCreateAccountFlow, + .sshKeyVaultItem, .testLocalFeatureFlag, .testLocalInitialBoolFlag, .testLocalInitialIntFlag, diff --git a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlagTests.swift b/BitwardenShared/Core/Platform/Models/Enum/FeatureFlagTests.swift index aa16239ef..75a8489f5 100644 --- a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlagTests.swift +++ b/BitwardenShared/Core/Platform/Models/Enum/FeatureFlagTests.swift @@ -12,6 +12,25 @@ final class FeatureFlagTests: BitwardenTestCase { XCTAssertEqual(filtered, []) } + /// `getter:isRemotelyConfigured` returns the correct value for each flag. + func test_isRemotelyConfigured() { + XCTAssertTrue(FeatureFlag.emailVerification.isRemotelyConfigured) + XCTAssertTrue(FeatureFlag.testRemoteInitialBoolFlag.isRemotelyConfigured) + XCTAssertTrue(FeatureFlag.testRemoteInitialIntFlag.isRemotelyConfigured) + XCTAssertTrue(FeatureFlag.testRemoteInitialStringFlag.isRemotelyConfigured) + + XCTAssertFalse(FeatureFlag.enableAuthenticatorSync.isRemotelyConfigured) + XCTAssertFalse(FeatureFlag.enableCipherKeyEncryption.isRemotelyConfigured) + XCTAssertFalse(FeatureFlag.importLoginsFlow.isRemotelyConfigured) + XCTAssertFalse(FeatureFlag.nativeCarouselFlow.isRemotelyConfigured) + XCTAssertFalse(FeatureFlag.nativeCreateAccountFlow.isRemotelyConfigured) + XCTAssertFalse(FeatureFlag.sshKeyVaultItem.isRemotelyConfigured) + XCTAssertFalse(FeatureFlag.testLocalFeatureFlag.isRemotelyConfigured) + XCTAssertFalse(FeatureFlag.testLocalInitialBoolFlag.isRemotelyConfigured) + XCTAssertFalse(FeatureFlag.testLocalInitialIntFlag.isRemotelyConfigured) + XCTAssertFalse(FeatureFlag.testLocalInitialStringFlag.isRemotelyConfigured) + } + /// `name` formats the raw value of a feature flag func test_name() { XCTAssertEqual(FeatureFlag.testLocalFeatureFlag.name, "Test Local Feature Flag") diff --git a/BitwardenShared/Core/Vault/Extensions/BitwardenSdk+Vault.swift b/BitwardenShared/Core/Vault/Extensions/BitwardenSdk+Vault.swift index b956d0e84..647862425 100644 --- a/BitwardenShared/Core/Vault/Extensions/BitwardenSdk+Vault.swift +++ b/BitwardenShared/Core/Vault/Extensions/BitwardenSdk+Vault.swift @@ -390,8 +390,7 @@ extension BitwardenSdk.CipherType { case .identity: self = .identity case .sshKey: - // TODO: PM-10401 set self = .sshKey when SDK is ready. - self = .init(rawValue: 5)! + self = .sshKey } } } diff --git a/BitwardenShared/Core/Vault/Extensions/BitwardenSdkVaultTests.swift b/BitwardenShared/Core/Vault/Extensions/BitwardenSdkVaultTests.swift index b4fcfdc09..e98e39cd0 100644 --- a/BitwardenShared/Core/Vault/Extensions/BitwardenSdkVaultTests.swift +++ b/BitwardenShared/Core/Vault/Extensions/BitwardenSdkVaultTests.swift @@ -5,6 +5,21 @@ import XCTest @testable import BitwardenShared +// MARK: - BitwardenSdk.CipherType + +class BitwardenSdkVaultBitwardenCipherTypeTests: BitwardenTestCase { // swiftlint:disable:this type_name + // MARK: Tests + + /// `init(type:)` initializes the SDK cipher type based on the cipher type. + func test_init_byCipherType() { + XCTAssertEqual(BitwardenSdk.CipherType(.login), .login) + XCTAssertEqual(BitwardenSdk.CipherType(.card), .card) + XCTAssertEqual(BitwardenSdk.CipherType(.identity), .identity) + XCTAssertEqual(BitwardenSdk.CipherType(.secureNote), .secureNote) + XCTAssertEqual(BitwardenSdk.CipherType(.sshKey), .sshKey) + } +} + // MARK: - Cipher class BitwardenSdkVaultCipherTests: BitwardenTestCase { diff --git a/BitwardenShared/Core/Vault/Models/Enum/CipherType.swift b/BitwardenShared/Core/Vault/Models/Enum/CipherType.swift index aaac24115..86edcfa00 100644 --- a/BitwardenShared/Core/Vault/Models/Enum/CipherType.swift +++ b/BitwardenShared/Core/Vault/Models/Enum/CipherType.swift @@ -32,6 +32,8 @@ extension CipherType { self = .login case .secureNote: self = .secureNote + case .sshKey: + self = .sshKey case .collection, .folder, .noFolder, @@ -61,4 +63,14 @@ extension CipherType: Menuable { extension CipherType { /// These are the cases of `CipherType` that the user can use to create a cipher. static let canCreateCases: [CipherType] = [.login, .card, .identity, .secureNote] + + /// The allowed custom field types per cipher type. + var allowedFieldTypes: [FieldType] { + switch self { + case .card, .identity, .login: + return [.text, .hidden, .boolean, .linked] + case .secureNote, .sshKey: + return [.text, .hidden, .boolean] + } + } } diff --git a/BitwardenShared/Core/Vault/Models/Enum/CipherTypeTests.swift b/BitwardenShared/Core/Vault/Models/Enum/CipherTypeTests.swift index e5f160171..2a30c52d9 100644 --- a/BitwardenShared/Core/Vault/Models/Enum/CipherTypeTests.swift +++ b/BitwardenShared/Core/Vault/Models/Enum/CipherTypeTests.swift @@ -5,12 +5,22 @@ import XCTest class CipherTypeTests: BitwardenTestCase { // MARK: Tests + /// `getter:allowedFieldTypes` return the correct `FielldType` array for the given cipher type.. + func test_allowedFieldTypes() { + XCTAssertEqual(CipherType.login.allowedFieldTypes, [.text, .hidden, .boolean, .linked]) + XCTAssertEqual(CipherType.card.allowedFieldTypes, [.text, .hidden, .boolean, .linked]) + XCTAssertEqual(CipherType.identity.allowedFieldTypes, [.text, .hidden, .boolean, .linked]) + XCTAssertEqual(CipherType.secureNote.allowedFieldTypes, [.text, .hidden, .boolean]) + XCTAssertEqual(CipherType.sshKey.allowedFieldTypes, [.text, .hidden, .boolean]) + } + /// `localizedName` returns the correct values. func test_localizedName() { XCTAssertEqual(CipherType.card.localizedName, Localizations.typeCard) XCTAssertEqual(CipherType.identity.localizedName, Localizations.typeIdentity) XCTAssertEqual(CipherType.login.localizedName, Localizations.typeLogin) XCTAssertEqual(CipherType.secureNote.localizedName, Localizations.typeSecureNote) + XCTAssertEqual(CipherType.sshKey.localizedName, Localizations.sshKey) } /// `init` with a `VaultListGroup` produces the correct value. @@ -21,6 +31,7 @@ class CipherTypeTests: BitwardenTestCase { XCTAssertEqual(CipherType(group: .identity), .identity) XCTAssertEqual(CipherType(group: .login), .login) XCTAssertEqual(CipherType(group: .secureNote), .secureNote) + XCTAssertEqual(CipherType(group: .sshKey), .sshKey) XCTAssertNil(CipherType(group: .trash)) } diff --git a/BitwardenShared/Core/Vault/Models/Request/CipherRequestModel.swift b/BitwardenShared/Core/Vault/Models/Request/CipherRequestModel.swift index 132bedba4..94584d87d 100644 --- a/BitwardenShared/Core/Vault/Models/Request/CipherRequestModel.swift +++ b/BitwardenShared/Core/Vault/Models/Request/CipherRequestModel.swift @@ -56,6 +56,9 @@ struct CipherRequestModel: JSONRequestBody { /// Secure note data if the cipher is a secure note. let secureNote: CipherSecureNoteModel? + /// SSH key data if the cipher is an SSH Key. + let sshKey: CipherSSHKeyModel? + /// The type of the cipher. let type: CipherType } @@ -85,6 +88,7 @@ extension CipherRequestModel { passwordHistory: cipher.passwordHistory?.map(CipherPasswordHistoryModel.init), reprompt: CipherRepromptType(type: cipher.reprompt), secureNote: cipher.secureNote.map(CipherSecureNoteModel.init), + sshKey: cipher.sshKey.map(CipherSSHKeyModel.init), type: CipherType(type: cipher.type) ) } diff --git a/BitwardenShared/Core/Vault/Repositories/VaultRepository.swift b/BitwardenShared/Core/Vault/Repositories/VaultRepository.swift index 380b94b1d..f82fbff9f 100644 --- a/BitwardenShared/Core/Vault/Repositories/VaultRepository.swift +++ b/BitwardenShared/Core/Vault/Repositories/VaultRepository.swift @@ -514,6 +514,11 @@ class DefaultVaultRepository { // swiftlint:disable:this type_body_length ? { $0.deletedDate == nil } : { $0.deletedDate != nil } + let isSSHKeyVaultItemEnabled: Bool = await configService.getFeatureFlag(.sshKeyVaultItem) + let sshKeyFilter: (CipherView) -> Bool = { cipher in + cipher.type != .sshKey || isSSHKeyVaultItemEnabled + } + return try await cipherService.ciphersPublisher().asyncTryMap { ciphers -> [CipherView] in // Convert the Ciphers to CipherViews and filter appropriately. let matchingCiphers = try await ciphers.asyncMap { cipher in @@ -522,6 +527,7 @@ class DefaultVaultRepository { // swiftlint:disable:this type_body_length .filter { cipher in filterType.cipherFilter(cipher) && isMatchingCipher(cipher) && + sshKeyFilter(cipher) && (cipherFilter?(cipher) ?? true) } @@ -739,6 +745,8 @@ class DefaultVaultRepository { // swiftlint:disable:this type_body_length items = activeCiphers.filter { $0.folderId == nil }.compactMap(VaultListItem.init) case .secureNote: items = activeCiphers.filter { $0.type == .secureNote }.compactMap(VaultListItem.init) + case .sshKey: + items = activeCiphers.filter { $0.type == .sshKey }.compactMap(VaultListItem.init) case .totp: items = try await totpListItems(from: activeCiphers, filter: filter) case .trash: @@ -830,7 +838,11 @@ class DefaultVaultRepository { // swiftlint:disable:this type_body_length .filter(filter.cipherFilter) .sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending } - let activeCiphers = ciphers.filter { $0.deletedDate == nil } + let isSSHKeyVaultItemFlagEnabled: Bool = await configService.getFeatureFlag(.sshKeyVaultItem) + let activeCiphers = ciphers.filter { cipher in + cipher.deletedDate == nil + && (isSSHKeyVaultItemFlagEnabled || cipher.type != .sshKey) + } let folders = try await clientService.vault().folders() .decryptList(folders: folders) @@ -882,13 +894,18 @@ class DefaultVaultRepository { // swiftlint:disable:this type_body_length let typesLoginCount = activeCiphers.lazy.filter { $0.type == .login }.count let typesSecureNoteCount = activeCiphers.lazy.filter { $0.type == .secureNote }.count - let types = [ + var types = [ VaultListItem(id: "Types.Logins", itemType: .group(.login, typesLoginCount)), VaultListItem(id: "Types.Cards", itemType: .group(.card, typesCardCount)), VaultListItem(id: "Types.Identities", itemType: .group(.identity, typesIdentityCount)), VaultListItem(id: "Types.SecureNotes", itemType: .group(.secureNote, typesSecureNoteCount)), ] + if isSSHKeyVaultItemFlagEnabled { + let typesSSHKeyCount = activeCiphers.lazy.filter { $0.type == .sshKey }.count + types.append(VaultListItem(id: "Types.SSHKeys", itemType: .group(.sshKey, typesSSHKeyCount))) + } + return [ VaultListSection(id: "TOTP", items: totpItems, name: Localizations.totp), VaultListSection(id: "Favorites", items: ciphersFavorites, name: Localizations.favorites), @@ -1292,6 +1309,8 @@ extension DefaultVaultRepository: VaultRepository { return cipher.folderId == nil case .secureNote: return cipher.type == .secureNote + case .sshKey: + return cipher.type == .sshKey case .totp: return cipher.type == .login && cipher.login?.totp != nil diff --git a/BitwardenShared/Core/Vault/Repositories/VaultRepositoryTests.swift b/BitwardenShared/Core/Vault/Repositories/VaultRepositoryTests.swift index 6cb8c02d0..c012f56a1 100644 --- a/BitwardenShared/Core/Vault/Repositories/VaultRepositoryTests.swift +++ b/BitwardenShared/Core/Vault/Repositories/VaultRepositoryTests.swift @@ -1385,6 +1385,7 @@ class VaultRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_b name: "one time cafefe", type: .login ), + .fixture(id: "6", name: "Some sshkey", type: .sshKey), ] let cipherView = try CipherView(cipher: XCTUnwrap(cipherService.ciphersSubject.value[0])) let expectedSearchResult = try [XCTUnwrap(VaultListItem(cipherView: cipherView))] @@ -1421,6 +1422,7 @@ class VaultRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_b name: "one time cafefe", type: .login ), + .fixture(id: "6", name: "Some sshkey", type: .sshKey), ] let cipherView = try CipherView(cipher: XCTUnwrap(cipherService.ciphersSubject.value[3])) let expectedSearchResult = try [XCTUnwrap(VaultListItem(cipherView: cipherView))] @@ -1462,6 +1464,7 @@ class VaultRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_b name: "one time cafefe", type: .login ), + .fixture(id: "6", name: "Some sshkey", type: .sshKey), ] let cipherView = try CipherView(cipher: XCTUnwrap(cipherService.ciphersSubject.value[4])) let expectedSearchResult = try [XCTUnwrap(VaultListItem(cipherView: cipherView))] @@ -1507,6 +1510,7 @@ class VaultRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_b name: "one time cafefe", type: .login ), + .fixture(id: "6", name: "Some sshkey", type: .sshKey), ] let cipherView = try CipherView(cipher: XCTUnwrap(cipherService.ciphersSubject.value[4])) let expectedSearchResult = try [XCTUnwrap(VaultListItem(cipherView: cipherView))] @@ -1548,6 +1552,7 @@ class VaultRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_b name: "one time cafefe", type: .login ), + .fixture(id: "6", name: "Some sshkey", type: .sshKey), ] let expectedSearchResult = try [ XCTUnwrap( @@ -1599,6 +1604,7 @@ class VaultRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_b name: "one time cafefe", type: .login ), + .fixture(id: "6", name: "Some sshkey", type: .sshKey), ] let expectedSearchResult = try [ XCTUnwrap( @@ -1623,6 +1629,97 @@ class VaultRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_b XCTAssertEqual(ciphers, expectedSearchResult) } + /// `searchVaultListPublisher(searchText:, group: .sshKey, filterType:)` + /// returns search matching cipher name for SSH key items. + @MainActor + func test_searchVaultListPublisher_searchText_sshKey() async throws { + configService.featureFlagsBool[.sshKeyVaultItem] = true + stateService.activeAccount = .fixture() + cipherService.ciphersSubject.value = [ + .fixture(id: "1", name: "café", type: .card), + .fixture(id: "2", name: "cafepass", type: .login), + .fixture(deletedDate: .now, id: "3", name: "deleted Café"), + .fixture( + folderId: "coffee", + id: "0", + name: "Best Cafes", + type: .secureNote + ), + .fixture( + collectionIds: ["123", "meep"], + id: "4", + name: "Café Friend", + type: .identity + ), + .fixture(id: "5", name: "Café thoughts", type: .secureNote), + .fixture( + id: "6", + login: .fixture(totp: .standardTotpKey), + name: "one time cafefe", + type: .login + ), + .fixture(id: "7", name: "cafe", type: .sshKey), + ] + let expectedSearchResult = try [ + XCTUnwrap( + VaultListItem( + cipherView: CipherView(cipher: XCTUnwrap(cipherService.ciphersSubject.value[7])) + ) + ), + ] + var iterator = try await subject + .searchVaultListPublisher( + searchText: "cafe", + group: .sshKey, + filterType: .allVaults + ) + .makeAsyncIterator() + let ciphers = try await iterator.next() + XCTAssertEqual(ciphers, expectedSearchResult) + } + + /// `searchVaultListPublisher(searchText:, group: .sshKey, filterType:)` + /// returns 0 search matching cipher name for SSH key items when `.sshKeyVaultItem` flag is disabled. + @MainActor + func test_searchVaultListPublisher_searchText_sshKeyWithFlagDisabled() async throws { + configService.featureFlagsBool[.sshKeyVaultItem] = false + stateService.activeAccount = .fixture() + cipherService.ciphersSubject.value = [ + .fixture(id: "1", name: "café", type: .card), + .fixture(id: "2", name: "cafepass", type: .login), + .fixture(deletedDate: .now, id: "3", name: "deleted Café"), + .fixture( + folderId: "coffee", + id: "0", + name: "Best Cafes", + type: .secureNote + ), + .fixture( + collectionIds: ["123", "meep"], + id: "4", + name: "Café Friend", + type: .identity + ), + .fixture(id: "5", name: "Café thoughts", type: .secureNote), + .fixture( + id: "6", + login: .fixture(totp: .standardTotpKey), + name: "one time cafefe", + type: .login + ), + .fixture(id: "7", name: "cafe", type: .sshKey), + ] + var iterator = try await subject + .searchVaultListPublisher( + searchText: "cafe", + group: .sshKey, + filterType: .allVaults + ) + .makeAsyncIterator() + let ciphers = try await iterator.next() + XCTAssertEqual(ciphers, []) + } + /// `searchVaultListPublisher(searchText:, group: .totp, filterType:)` /// returns search matching cipher name for TOTP login items. func test_searchVaultListPublisher_searchText_totp() async throws { @@ -2235,6 +2332,26 @@ class VaultRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_b ) } + /// `vaultListPublisher(group:filter:)` returns a publisher for the vault list items on SSH key group. + func test_vaultListPublisher_groups_sshKey() async throws { + let cipher = Cipher.fixture(id: "1", type: .sshKey) + cipherService.ciphersSubject.send([cipher]) + + var iterator = try await subject.vaultListPublisher(group: .sshKey, filter: .allVaults).makeAsyncIterator() + let vaultListSections = try await iterator.next() + + XCTAssertEqual( + vaultListSections, + [ + VaultListSection( + id: "Items", + items: [.fixture(cipherView: .init(cipher: cipher))], + name: Localizations.items + ), + ] + ) + } + /// `vaultListPublisher(group:filter:)` returns a publisher for the vault list items for premium accounts. func test_vaultListPublisher_groups_totp_premium() async throws { stateService.activeAccount = premiumAccount @@ -2682,6 +2799,47 @@ class VaultRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_b } } + /// `vaultListPublisher()` returns a publisher for the list of sections and items that are + /// displayed in the vault for a vault that contains collections and folders, with no filter. + @MainActor + func test_vaultListPublisher_withCollections_allWithSSHKeyFlagEnabled() async throws { + configService.featureFlagsBool[.sshKeyVaultItem] = true + stateService.activeAccount = .fixture() + let syncResponse = try JSONDecoder.defaultDecoder.decode( + SyncResponseModel.self, + from: APITestData.syncWithCiphersCollections.data + ) + cipherService.ciphersSubject.send(syncResponse.ciphers.compactMap(Cipher.init)) + collectionService.collectionsSubject.send(syncResponse.collections.compactMap(Collection.init)) + folderService.foldersSubject.send(syncResponse.folders.compactMap(Folder.init)) + + var iterator = try await subject.vaultListPublisher(filter: .allVaults).makeAsyncIterator() + let sections = try await iterator.next() + + try assertInlineSnapshot(of: dumpVaultListSections(XCTUnwrap(sections)), as: .lines) { + """ + Section: Favorites + - Cipher: Apple + Section: Types + - Group: Login (6) + - Group: Card (1) + - Group: Identity (1) + - Group: Secure note (1) + - Group: SSH key (1) + Section: Folders + - Group: Development (0) + - Group: Internal (1) + - Group: Social (2) + - Group: No Folder (6) + Section: Collections + - Group: Design (2) + - Group: Engineering (3) + Section: Trash + - Group: Trash (1) + """ + } + } + /// `vaultListPublisher()` returns a publisher for the list of sections and items that are /// displayed in the vault for a vault that contains collections with the my vault filter. func test_vaultListPublisher_withCollections_myVault() async throws { @@ -2716,6 +2874,44 @@ class VaultRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_b } } + /// `vaultListPublisher()` returns a publisher for the list of sections and items that are + /// displayed in the vault for a vault that contains collections with the my vault filter with SSH Key flag enabled. + @MainActor + func test_vaultListPublisher_withCollections_myVaultWithSSHKeyFlagEnabled() async throws { + configService.featureFlagsBool[.sshKeyVaultItem] = true + stateService.activeAccount = .fixture() + let syncResponse = try JSONDecoder.defaultDecoder.decode( + SyncResponseModel.self, + from: APITestData.syncWithCiphersCollections.data + ) + cipherService.ciphersSubject.send(syncResponse.ciphers.compactMap(Cipher.init)) + collectionService.collectionsSubject.send(syncResponse.collections.compactMap(Collection.init)) + folderService.foldersSubject.send(syncResponse.folders.compactMap(Folder.init)) + + var iterator = try await subject.vaultListPublisher(filter: .myVault).makeAsyncIterator() + let sections = try await iterator.next() + + try assertInlineSnapshot(of: dumpVaultListSections(XCTUnwrap(sections)), as: .lines) { + """ + Section: Types + - Group: Login (1) + - Group: Card (1) + - Group: Identity (1) + - Group: Secure note (1) + - Group: SSH key (1) + Section: Folders + - Group: Social (1) + Section: No Folder + - Cipher: Bitwarden User + - Cipher: Top Secret Note + - Cipher: Top SSH Key + - Cipher: Visa + Section: Trash + - Group: Trash (1) + """ + } + } + /// `vaultListPublisher()` returns a publisher for the list of sections and items that are /// displayed in the vault for a vault that contains collections with the organization filter. func test_vaultListPublisher_withCollections_organization() async throws { diff --git a/BitwardenShared/Core/Vault/Services/API/Fixtures/syncWithCiphers.json b/BitwardenShared/Core/Vault/Services/API/Fixtures/syncWithCiphers.json index f1442ba9c..4f6a4e054 100644 --- a/BitwardenShared/Core/Vault/Services/API/Fixtures/syncWithCiphers.json +++ b/BitwardenShared/Core/Vault/Services/API/Fixtures/syncWithCiphers.json @@ -132,6 +132,24 @@ "collectionIds": [], "creationDate": "2023-07-04T05:00:53Z", "edit": true + }, + { + "sshKey": { + "privateKey": "privateKey", + "publicKey": "publicKey", + "keyFingerprint": "keyFingerprint" + }, + "viewPassword": true, + "id": "98f65684-f059-4cfd-9eb0-947b69b51882", + "favorite": false, + "reprompt": 0, + "name": "Top SSH Key", + "type": 5, + "organizationUseTotp": false, + "revisionDate": "2024-08-13T12:15:17Z", + "collectionIds": [], + "creationDate": "2024-07-04T05:00:53Z", + "edit": true } ], "collections": [], diff --git a/BitwardenShared/Core/Vault/Services/API/Fixtures/syncWithCiphersCollections.json b/BitwardenShared/Core/Vault/Services/API/Fixtures/syncWithCiphersCollections.json index f22f548c3..2443a25fb 100644 --- a/BitwardenShared/Core/Vault/Services/API/Fixtures/syncWithCiphersCollections.json +++ b/BitwardenShared/Core/Vault/Services/API/Fixtures/syncWithCiphersCollections.json @@ -245,6 +245,24 @@ ], "edit": true, "creationDate": "2023-12-01T15:49:55Z" + }, + { + "sshKey": { + "privateKey": "privateKey", + "publicKey": "publicKey", + "keyFingerprint": "keyFingerprint" + }, + "viewPassword": true, + "id": "98f65684-f059-4cfd-9eb0-947b69b51882", + "favorite": false, + "reprompt": 0, + "name": "Top SSH Key", + "type": 5, + "organizationUseTotp": false, + "revisionDate": "2024-08-13T12:15:17Z", + "collectionIds": [], + "creationDate": "2024-07-04T05:00:53Z", + "edit": true } ], "collections": [ diff --git a/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Icons/key24.imageset/Contents.json b/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Icons/key24.imageset/Contents.json new file mode 100644 index 000000000..19c3c6b1a --- /dev/null +++ b/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Icons/key24.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "key24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Icons/key24.imageset/key24.pdf b/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Icons/key24.imageset/key24.pdf new file mode 100644 index 000000000..cc1b5ce24 Binary files /dev/null and b/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Icons/key24.imageset/key24.pdf differ diff --git a/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings b/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings index bbf132ffd..b4f3f8608 100644 --- a/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings +++ b/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings @@ -1058,3 +1058,4 @@ "CopyPublicKey" = "Copy public key"; "CopyPrivateKey" = "Copy private key"; "CopyFingerprint" = "Copy fingerprint"; +"SSHKeys" = "SSH keys"; diff --git a/BitwardenShared/UI/Vault/Vault/VaultGroup/VaultGroupState.swift b/BitwardenShared/UI/Vault/Vault/VaultGroup/VaultGroupState.swift index 3a926ede9..ee04f140d 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultGroup/VaultGroupState.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultGroup/VaultGroupState.swift @@ -60,21 +60,22 @@ struct VaultGroupState: Equatable, Sendable { // Don't show if there is data. guard emptyData else { return false } - // If the collection or trash are empty, return false. - if case .collection = group { - return false - } else if case .trash = group { + switch group { + case .collection, .sshKey, .trash: return false + default: + return true } - return true } /// Whether to show the add item floating action button. var showAddItemFloatingActionButton: Bool { - if case .trash = group { + switch group { + case .sshKey, .trash: return false + default: + return true } - return true } /// Whether to show the special web icons. diff --git a/BitwardenShared/UI/Vault/Vault/VaultGroup/VaultGroupViewTests.swift b/BitwardenShared/UI/Vault/Vault/VaultGroup/VaultGroupViewTests.swift index ba36205d7..85fd8ca2e 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultGroup/VaultGroupViewTests.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultGroup/VaultGroupViewTests.swift @@ -133,6 +133,13 @@ class VaultGroupViewTests: BitwardenTestCase { assertSnapshot(of: subject, as: .defaultPortrait) } + @MainActor + func test_snapshot_emptySSHKey() { + processor.state.group = .sshKey + processor.state.loadingState = .data([]) + assertSnapshot(of: subject, as: .defaultPortrait) + } + @MainActor func test_snapshot_emptyTrash() { processor.state.group = .trash diff --git a/BitwardenShared/UI/Vault/Vault/VaultGroup/__Snapshots__/VaultGroupViewTests/test_snapshot_emptySSHKey.1.png b/BitwardenShared/UI/Vault/Vault/VaultGroup/__Snapshots__/VaultGroupViewTests/test_snapshot_emptySSHKey.1.png new file mode 100644 index 000000000..b96465cbd Binary files /dev/null and b/BitwardenShared/UI/Vault/Vault/VaultGroup/__Snapshots__/VaultGroupViewTests/test_snapshot_emptySSHKey.1.png differ diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListGroup.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListGroup.swift index 6b57ee3db..05803fd68 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListGroup.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListGroup.swift @@ -17,6 +17,9 @@ public enum VaultListGroup: Equatable, Hashable, Sendable { /// A group of secure note type ciphers. case secureNote + /// A group of SSH key type ciphers. + case sshKey + /// A group of TOTP Enabled login types. case totp @@ -73,6 +76,8 @@ extension VaultListGroup { return Localizations.typeLogin case .secureNote: return Localizations.typeSecureNote + case .sshKey: + return Localizations.sshKey case .totp: return Localizations.verificationCodes case .trash: @@ -97,6 +102,8 @@ extension VaultListGroup { return Localizations.logins case .secureNote: return Localizations.secureNotes + case .sshKey: + return Localizations.sshKeys case .totp: return Localizations.verificationCodes case .trash: diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListGroupTests.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListGroupTests.swift index 9245acd42..8299c677d 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListGroupTests.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListGroupTests.swift @@ -18,6 +18,7 @@ class VaultListGroupTests: BitwardenTestCase { XCTAssertNil(VaultListGroup.identity.collectionId) XCTAssertNil(VaultListGroup.login.collectionId) XCTAssertNil(VaultListGroup.secureNote.collectionId) + XCTAssertNil(VaultListGroup.sshKey.collectionId) XCTAssertNil(VaultListGroup.totp.collectionId) XCTAssertNil(VaultListGroup.trash.collectionId) } @@ -31,6 +32,7 @@ class VaultListGroupTests: BitwardenTestCase { XCTAssertFalse(VaultListGroup.login.isFolder) XCTAssertFalse(VaultListGroup.noFolder.isFolder) XCTAssertFalse(VaultListGroup.secureNote.isFolder) + XCTAssertFalse(VaultListGroup.sshKey.isFolder) XCTAssertFalse(VaultListGroup.totp.isFolder) XCTAssertFalse(VaultListGroup.trash.isFolder) } @@ -45,6 +47,7 @@ class VaultListGroupTests: BitwardenTestCase { XCTAssertNil(VaultListGroup.identity.folderId) XCTAssertNil(VaultListGroup.login.folderId) XCTAssertNil(VaultListGroup.secureNote.folderId) + XCTAssertNil(VaultListGroup.sshKey.folderId) XCTAssertNil(VaultListGroup.totp.folderId) XCTAssertNil(VaultListGroup.trash.folderId) } @@ -60,6 +63,7 @@ class VaultListGroupTests: BitwardenTestCase { XCTAssertEqual(VaultListGroup.identity.name, "Identity") XCTAssertEqual(VaultListGroup.login.name, "Login") XCTAssertEqual(VaultListGroup.secureNote.name, "Secure note") + XCTAssertEqual(VaultListGroup.sshKey.name, "SSH key") XCTAssertEqual(VaultListGroup.totp.name, Localizations.verificationCodes) XCTAssertEqual(VaultListGroup.trash.name, "Trash") } @@ -75,6 +79,7 @@ class VaultListGroupTests: BitwardenTestCase { XCTAssertEqual(VaultListGroup.identity.navigationTitle, Localizations.identities) XCTAssertEqual(VaultListGroup.login.navigationTitle, Localizations.logins) XCTAssertEqual(VaultListGroup.secureNote.navigationTitle, Localizations.secureNotes) + XCTAssertEqual(VaultListGroup.sshKey.navigationTitle, Localizations.sshKeys) XCTAssertEqual(VaultListGroup.totp.navigationTitle, Localizations.verificationCodes) XCTAssertEqual(VaultListGroup.trash.navigationTitle, Localizations.trash) } @@ -90,6 +95,7 @@ class VaultListGroupTests: BitwardenTestCase { XCTAssertNil(VaultListGroup.identity.organizationId) XCTAssertNil(VaultListGroup.login.organizationId) XCTAssertNil(VaultListGroup.secureNote.organizationId) + XCTAssertNil(VaultListGroup.sshKey.organizationId) XCTAssertNil(VaultListGroup.totp.organizationId) XCTAssertNil(VaultListGroup.trash.organizationId) } diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItem.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItem.swift index 0bf80f7b5..fd9e3b105 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItem.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItem.swift @@ -94,7 +94,7 @@ extension VaultListItem { case .secureNote: Asset.Images.file24 case .sshKey: - Asset.Images.key16 + Asset.Images.key24 } case let .group(group, _): switch group { @@ -111,6 +111,8 @@ extension VaultListItem { Asset.Images.globe24 case .secureNote: Asset.Images.file24 + case .sshKey: + Asset.Images.key24 case .totp: Asset.Images.clock24 case .trash: diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItemTests.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItemTests.swift index 22fa8c79b..cbf894fd6 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItemTests.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItemTests.swift @@ -144,7 +144,7 @@ class VaultListItemTests: BitwardenTestCase { // swiftlint:disable:this type_bod ) XCTAssertEqual( VaultListItem(cipherView: .fixture(type: .sshKey))?.icon.name, - Asset.Images.key16.name + Asset.Images.key24.name ) XCTAssertEqual( @@ -171,6 +171,10 @@ class VaultListItemTests: BitwardenTestCase { // swiftlint:disable:this type_bod VaultListItem(id: "", itemType: .group(.secureNote, 1)).icon.name, Asset.Images.file24.name ) + XCTAssertEqual( + VaultListItem(id: "", itemType: .group(.sshKey, 1)).icon.name, + Asset.Images.key24.name + ) XCTAssertEqual( VaultListItem(id: "", itemType: .group(.totp, 1)).icon.name, Asset.Images.clock24.name diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListView.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListView.swift index 1e400f438..29c4fec57 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListView.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListView.swift @@ -490,6 +490,10 @@ struct VaultListView_Previews: PreviewProvider { id: "24", itemType: .group(.secureNote, 0) ), + VaultListItem( + id: "25", + itemType: .group(.sshKey, 4) + ), ], name: "Types" ), diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditItemProcessor.swift b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditItemProcessor.swift index dc3891d45..b68b0609e 100644 --- a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditItemProcessor.swift +++ b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditItemProcessor.swift @@ -470,9 +470,7 @@ final class AddEditItemProcessor: StateProcessor