Skip to content

Commit 8cba062

Browse files
BIT-2407: Update cipher on server when SDK adds a cipher key (#667)
1 parent 407f314 commit 8cba062

File tree

7 files changed

+138
-13
lines changed

7 files changed

+138
-13
lines changed

BitwardenShared/Core/Vault/Repositories/VaultRepository.swift

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,23 @@ class DefaultVaultRepository { // swiftlint:disable:this type_body_length
375375

376376
// MARK: Private
377377

378+
/// Encrypts the cipher. If the cipher was migrated by the SDK (e.g. added a cipher key), the
379+
/// cipher will be updated locally and on the server.
380+
///
381+
/// - Parameter cipherView: The cipher to encrypt.
382+
/// - Returns: The encrypted cipher.
383+
///
384+
private func encryptAndUpdateCipher(_ cipherView: CipherView) async throws -> Cipher {
385+
let cipher = try await clientService.vault().ciphers().encrypt(cipherView: cipherView)
386+
387+
let didAddCipherKey = cipherView.key == nil && cipher.key != nil
388+
if didAddCipherKey {
389+
try await cipherService.updateCipherWithServer(cipher)
390+
}
391+
392+
return cipher
393+
}
394+
378395
/// Returns a list of `VaultListItem`s for the folders within a nested tree. By default, this
379396
/// will return the list items for the folders at the root of the tree. Specifying a
380397
/// `nestedFolderId` will return the list items for the children of the folder with the
@@ -952,7 +969,7 @@ extension DefaultVaultRepository: VaultRepository {
952969
else { throw BitwardenError.dataError("Missing data") }
953970

954971
// Get the encrypted cipher and attachment, then download the actual data of the attachment.
955-
let encryptedCipher = try await clientService.vault().ciphers().encrypt(cipherView: cipher)
972+
let encryptedCipher = try await encryptAndUpdateCipher(cipher)
956973
guard let attachment = encryptedCipher.attachments?.first(where: { $0.id == attachmentId }),
957974
let downloadedUrl = try await cipherService.downloadAttachment(withId: attachmentId, cipherId: cipherId)
958975
else { return nil }
@@ -1032,7 +1049,7 @@ extension DefaultVaultRepository: VaultRepository {
10321049
func restoreCipher(_ cipher: BitwardenSdk.CipherView) async throws {
10331050
guard let id = cipher.id else { throw CipherAPIServiceError.updateMissingId }
10341051
let restoredCipher = cipher.update(deletedDate: nil)
1035-
let encryptCipher = try await clientService.vault().ciphers().encrypt(cipherView: restoredCipher)
1052+
let encryptCipher = try await encryptAndUpdateCipher(restoredCipher)
10361053
try await cipherService.restoreCipherWithServer(id: id, encryptCipher)
10371054
}
10381055

@@ -1048,7 +1065,7 @@ extension DefaultVaultRepository: VaultRepository {
10481065
)
10491066

10501067
// Encrypt the attachment.
1051-
let cipher = try await clientService.vault().ciphers().encrypt(cipherView: cipherView)
1068+
let cipher = try await encryptAndUpdateCipher(cipherView)
10521069
let attachment = try await clientService.vault().attachments().encryptBuffer(
10531070
cipher: cipher,
10541071
attachment: attachmentView,
@@ -1091,8 +1108,8 @@ extension DefaultVaultRepository: VaultRepository {
10911108
func softDeleteCipher(_ cipher: CipherView) async throws {
10921109
guard let id = cipher.id else { throw CipherAPIServiceError.updateMissingId }
10931110
let softDeletedCipher = cipher.update(deletedDate: timeProvider.presentTime)
1094-
let encryptCipher = try await clientService.vault().ciphers().encrypt(cipherView: softDeletedCipher)
1095-
try await cipherService.softDeleteCipherWithServer(id: id, encryptCipher)
1111+
let encryptedCipher = try await encryptAndUpdateCipher(softDeletedCipher)
1112+
try await cipherService.softDeleteCipherWithServer(id: id, encryptedCipher)
10961113
}
10971114

10981115
func updateCipher(_ cipherView: CipherView) async throws {
@@ -1101,7 +1118,7 @@ extension DefaultVaultRepository: VaultRepository {
11011118
}
11021119

11031120
func updateCipherCollections(_ cipherView: CipherView) async throws {
1104-
let cipher = try await clientService.vault().ciphers().encrypt(cipherView: cipherView)
1121+
let cipher = try await encryptAndUpdateCipher(cipherView)
11051122
try await cipherService.updateCipherCollectionsWithServer(cipher)
11061123
}
11071124

BitwardenShared/Core/Vault/Repositories/VaultRepositoryTests.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,29 @@ class VaultRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_b
228228
}
229229
}
230230

231+
/// `downloadAttachment(_:cipher:)` updates the cipher on the server if the SDK adds a cipher key.
232+
func test_downloadAttachment_updatesMigratedCipher() async throws {
233+
stateService.activeAccount = .fixture()
234+
let downloadUrl = FileManager.default.temporaryDirectory.appendingPathComponent("sillyGoose.txt")
235+
try Data("🪿".utf8).write(to: downloadUrl)
236+
cipherService.downloadAttachmentResult = .success(downloadUrl)
237+
let attachment = AttachmentView.fixture(fileName: "sillyGoose.txt")
238+
let cipherView = CipherView.fixture(attachments: [attachment])
239+
let cipher = Cipher.fixture(
240+
attachments: [Attachment(attachmentView: attachment)],
241+
key: "new key"
242+
)
243+
clientCiphers.encryptCipherResult = .success(cipher)
244+
245+
let resultUrl = try await subject.downloadAttachment(attachment, cipher: cipherView)
246+
247+
XCTAssertEqual(clientService.mockVault.clientCiphers.encryptedCiphers.last, cipherView)
248+
XCTAssertEqual(cipherService.downloadAttachmentId, attachment.id)
249+
XCTAssertEqual(clientService.mockVault.clientAttachments.encryptedFilePaths.last, downloadUrl.path)
250+
XCTAssertEqual(cipherService.updateCipherWithServerCiphers, [cipher])
251+
XCTAssertEqual(resultUrl?.lastPathComponent, "sillyGoose.txt")
252+
}
253+
231254
/// `fetchCipher(withId:)` returns the cipher if it exists and `nil` otherwise.
232255
func test_fetchCipher() async throws {
233256
var cipher = try await subject.fetchCipher(withId: "1")
@@ -1235,6 +1258,19 @@ class VaultRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_b
12351258
XCTAssertEqual(cipherService.restoredCipherId, "123")
12361259
}
12371260

1261+
/// `restoreCipher(_:cipher:)` updates the cipher on the server if the SDK adds a cipher key.
1262+
func test_restoreCipher_updatesMigratedCipher() async throws {
1263+
stateService.activeAccount = .fixture()
1264+
let cipherView = CipherView.fixture(deletedDate: .now)
1265+
let cipher = Cipher.fixture(key: "new key")
1266+
clientCiphers.encryptCipherResult = .success(cipher)
1267+
1268+
try await subject.restoreCipher(cipherView)
1269+
1270+
XCTAssertEqual(cipherService.restoredCipher, cipher)
1271+
XCTAssertEqual(cipherService.updateCipherWithServerCiphers, [cipher])
1272+
}
1273+
12381274
/// `saveAttachment(cipherView:fileData:fileName:)` saves the attachment to the cipher.
12391275
func test_saveAttachment() async throws {
12401276
cipherService.saveAttachmentWithServerResult = .success(.fixture(id: "42"))
@@ -1253,6 +1289,25 @@ class VaultRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_b
12531289
XCTAssertEqual(updatedCipher.id, "42")
12541290
}
12551291

1292+
/// `saveAttachment(cipherView:fileData:fileName:)` updates the cipher on the server if the SDK adds a cipher key.
1293+
func test_saveAttachment_updatesMigratedCipher() async throws {
1294+
cipherService.saveAttachmentWithServerResult = .success(.fixture(id: "42"))
1295+
let cipher = Cipher.fixture(key: "new key")
1296+
clientCiphers.encryptCipherResult = .success(cipher)
1297+
1298+
let updatedCipher = try await subject.saveAttachment(
1299+
cipherView: .fixture(),
1300+
fileData: Data(),
1301+
fileName: "Pineapple on pizza"
1302+
)
1303+
1304+
XCTAssertEqual(clientService.mockVault.clientCiphers.encryptedCiphers, [.fixture()])
1305+
XCTAssertEqual(clientService.mockVault.clientAttachments.encryptedBuffers, [Data()])
1306+
XCTAssertEqual(cipherService.updateCipherWithServerCiphers, [cipher])
1307+
XCTAssertEqual(cipherService.saveAttachmentWithServerCipher, cipher)
1308+
XCTAssertEqual(updatedCipher.id, "42")
1309+
}
1310+
12561311
/// `softDeleteCipher()` throws on id errors.
12571312
func test_softDeleteCipher_idError_nil() async throws {
12581313
stateService.accounts = [.fixtureAccountLogin()]
@@ -1275,6 +1330,19 @@ class VaultRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_b
12751330
XCTAssertEqual(cipherService.softDeleteCipherId, "123")
12761331
}
12771332

1333+
/// `softDeleteCipher(_:cipher:)` updates the cipher on the server if the SDK adds a cipher key.
1334+
func test_softDeleteCipher_updatesMigratedCipher() async throws {
1335+
stateService.activeAccount = .fixture()
1336+
let cipherView = CipherView.fixture(deletedDate: .now)
1337+
let cipher = Cipher.fixture(key: "new key")
1338+
clientCiphers.encryptCipherResult = .success(cipher)
1339+
1340+
try await subject.softDeleteCipher(cipherView)
1341+
1342+
XCTAssertEqual(cipherService.softDeleteCipher, cipher)
1343+
XCTAssertEqual(cipherService.updateCipherWithServerCiphers, [cipher])
1344+
}
1345+
12781346
/// `vaultListPublisher(group:filter:)` returns a publisher for the vault list items.
12791347
func test_vaultListPublisher_groups_card() async throws {
12801348
let cipher = Cipher.fixture(id: "1", type: .card)

BitwardenShared/Core/Vault/Services/SyncService.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,8 @@ extension DefaultSyncService {
272272
try await folderService.deleteFolderWithLocalStorage(id: data.id)
273273

274274
let updatedCiphers = try await cipherService.fetchAllCiphers()
275-
.asyncMap { try await clientService.vault().ciphers().decrypt(cipher: $0) }
275+
.filter { $0.folderId == data.id }
276276
.map { $0.update(folderId: nil) }
277-
.asyncMap { try await clientService.vault().ciphers().encrypt(cipherView: $0) }
278277

279278
for cipher in updatedCiphers {
280279
try await cipherService.updateCipherWithLocalStorage(cipher)

BitwardenShared/Core/Vault/Services/SyncServiceTests.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,10 @@ class SyncServiceTests: BitwardenTestCase {
606606
func test_deleteFolder() async throws {
607607
stateService.activeAccount = .fixture()
608608
folderService.deleteFolderWithLocalStorageResult = .success(())
609-
cipherService.fetchAllCiphersResult = .success([.fixture(folderId: "id")])
609+
cipherService.fetchAllCiphersResult = .success([
610+
.fixture(folderId: "id", id: "1"),
611+
.fixture(folderId: "other", id: "2"),
612+
])
610613
cipherService.updateCipherWithLocalStorageResult = .success(())
611614

612615
let notification = SyncFolderNotification(
@@ -616,7 +619,10 @@ class SyncServiceTests: BitwardenTestCase {
616619
)
617620
try await subject.deleteFolder(data: notification)
618621
XCTAssertEqual(folderService.deleteFolderWithLocalStorageId, "id")
619-
XCTAssertEqual(cipherService.updateCipherWithLocalStorageCipher, .fixture(folderId: nil))
622+
XCTAssertEqual(
623+
cipherService.updateCipherWithLocalStorageCiphers,
624+
[.fixture(folderId: nil, id: "1")]
625+
)
620626
}
621627

622628
func test_deleteSend() async throws {

BitwardenShared/Core/Vault/Services/TestHelpers/MockCipherService.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class MockCipherService: CipherService {
5151
var syncCipherWithServerId: String?
5252
var syncCipherWithServerResult: Result<Void, Error> = .success(())
5353

54-
var updateCipherWithLocalStorageCipher: BitwardenSdk.Cipher?
54+
var updateCipherWithLocalStorageCiphers = [BitwardenSdk.Cipher]()
5555
var updateCipherWithLocalStorageResult: Result<Void, Error> = .success(())
5656

5757
var updateCipherWithServerCiphers = [Cipher]()
@@ -135,7 +135,7 @@ class MockCipherService: CipherService {
135135
}
136136

137137
func updateCipherWithLocalStorage(_ cipher: Cipher) async throws {
138-
updateCipherWithLocalStorageCipher = cipher
138+
updateCipherWithLocalStorageCiphers.append(cipher)
139139
return try updateCipherWithLocalStorageResult.get()
140140
}
141141

BitwardenShared/Core/Vault/Services/TestHelpers/MockClientVaultService.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class MockClientAttachments: ClientAttachmentsProtocol {
8686
// MARK: - MockClientCiphers
8787

8888
class MockClientCiphers: ClientCiphersProtocol {
89+
var encryptCipherResult: Result<Cipher, Error>?
8990
var encryptError: Error?
9091
var encryptedCiphers = [CipherView]()
9192
var moveToOrganizationCipher: CipherView?
@@ -106,7 +107,7 @@ class MockClientCiphers: ClientCiphersProtocol {
106107
if let encryptError {
107108
throw encryptError
108109
}
109-
return Cipher(cipherView: cipherView)
110+
return try encryptCipherResult?.get() ?? Cipher(cipherView: cipherView)
110111
}
111112

112113
func moveToOrganization(

BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewLoginItem/Extensions/Cipher+Update.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,38 @@ extension Cipher {
3434
revisionDate: revisionDate
3535
)
3636
}
37+
38+
/// Returns a copy of the existing cipher with an updated folder ID.
39+
///
40+
/// - Parameter folderId: The folder ID to update in the cipher.
41+
/// - Returns: A copy of the existing cipher, with the specified properties updated.
42+
///
43+
func update(folderId: String?) -> Cipher {
44+
Cipher(
45+
id: id,
46+
organizationId: organizationId,
47+
folderId: folderId,
48+
collectionIds: collectionIds,
49+
key: key,
50+
name: name,
51+
notes: notes,
52+
type: type,
53+
login: login,
54+
identity: identity,
55+
card: card,
56+
secureNote: secureNote,
57+
favorite: favorite,
58+
reprompt: reprompt,
59+
organizationUseTotp: organizationUseTotp,
60+
edit: edit,
61+
viewPassword: viewPassword,
62+
localData: localData,
63+
attachments: attachments,
64+
fields: fields,
65+
passwordHistory: passwordHistory,
66+
creationDate: creationDate,
67+
deletedDate: deletedDate,
68+
revisionDate: revisionDate
69+
)
70+
}
3771
}

0 commit comments

Comments
 (0)