mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-11 13:02:14 -06:00
394 lines
15 KiB
Swift
394 lines
15 KiB
Swift
import BitwardenSdk
|
|
import Combine
|
|
import Foundation
|
|
|
|
// MARK: - CipherService
|
|
|
|
/// A protocol for a `CipherService` which manages syncing and updates to the user's ciphers.
|
|
///
|
|
protocol CipherService {
|
|
/// Adds a cipher for the current user both in the backend and in local storage.
|
|
///
|
|
/// - Parameters:
|
|
/// - cipher: The cipher to add.
|
|
/// - encryptedFor: The user ID who encrypted the `cipher`.
|
|
func addCipherWithServer(_ cipher: Cipher, encryptedFor: String) async throws
|
|
|
|
/// Returns the count of ciphers in the data store for the current user.
|
|
///
|
|
func cipherCount() async throws -> Int
|
|
|
|
/// Delete a cipher's attachment for the current user both in the backend and in local storage.
|
|
///
|
|
/// - Parameters:
|
|
/// - attachmentId: The id of the attachment to delete.
|
|
/// - cipherId: The id of the cipher that owns the attachment.
|
|
///
|
|
/// - Returns: The updated cipher with one less attachment.
|
|
///
|
|
func deleteAttachmentWithServer(attachmentId: String, cipherId: String) async throws -> Cipher?
|
|
|
|
/// Deletes a cipher for the current user both in the backend and in local storage.
|
|
///
|
|
/// - Parameter id: The id of cipher item to be deleted.
|
|
///
|
|
func deleteCipherWithServer(id: String) async throws
|
|
|
|
/// Deletes a cipher for the current user in local storage.
|
|
///
|
|
/// - Parameter id: The id of cipher item to be deleted.
|
|
///
|
|
func deleteCipherWithLocalStorage(id: String) async throws
|
|
|
|
/// Download the data of an attachment.
|
|
///
|
|
/// - Parameters:
|
|
/// - id: The id of the attachment to download.
|
|
/// - cipherId: The id of the cipher that owns the attachment.
|
|
///
|
|
/// - Returns: The url of the temporary file location if it was able to be downloaded.
|
|
///
|
|
func downloadAttachment(withId id: String, cipherId: String) async throws -> URL?
|
|
|
|
/// Attempt to fetch a cipher for the current user with the given id.
|
|
///
|
|
/// - Parameter id: The id of the cipher to find.
|
|
/// - Returns: The cipher if it was found and `nil` if not.
|
|
///
|
|
func fetchCipher(withId id: String) async throws -> Cipher?
|
|
|
|
/// Fetches all the ciphers for the current user.
|
|
///
|
|
/// - Returns: The ciphers belonging to the current user.
|
|
///
|
|
func fetchAllCiphers() async throws -> [Cipher]
|
|
|
|
/// Attempts to synchronize a cipher with the server.
|
|
///
|
|
/// This method fetches the updated cipher value from the server and updates the value in the
|
|
/// local storage.
|
|
///
|
|
/// - Parameter id: The id of the cipher to fetch.
|
|
///
|
|
func syncCipherWithServer(withId id: String) async throws
|
|
|
|
/// Replaces the persisted list of ciphers for the current user.
|
|
///
|
|
/// - Parameters:
|
|
/// - ciphers: The updated list of ciphers for the user.
|
|
/// - userId: The user ID associated with the ciphers.
|
|
///
|
|
func replaceCiphers(_ ciphers: [CipherDetailsResponseModel], userId: String) async throws
|
|
|
|
/// Restores a cipher from trash both in the backend and in local storage.
|
|
///
|
|
/// - Parameters:
|
|
/// - id: The id of the cipher to be restored.
|
|
/// - cipher: The cipher that the user is restoring.
|
|
///
|
|
func restoreCipherWithServer(id: String, _ cipher: Cipher) async throws
|
|
|
|
/// Save an attachment to a cipher for the current user, both in the backend and in local storage.
|
|
///
|
|
/// - Parameters:
|
|
/// - cipher: The cipher to add the attachment to.
|
|
/// - attachment: The encrypted attachment data to save.
|
|
///
|
|
/// - Returns: The updated cipher with one more attachment.
|
|
///
|
|
func saveAttachmentWithServer(cipher: Cipher, attachment: AttachmentEncryptResult) async throws -> Cipher
|
|
|
|
/// Shares a cipher with an organization and updates the locally stored data.
|
|
///
|
|
/// - Parameters:
|
|
/// - cipher: The cipher to share.
|
|
/// - encryptedFor: The user ID who encrypted the `cipher`.
|
|
func shareCipherWithServer(_ cipher: Cipher, encryptedFor: String) async throws
|
|
|
|
/// Soft deletes a cipher for the current user both in the backend and in local storage.
|
|
///
|
|
/// - Parameter cipher: The cipher item to be soft deleted.
|
|
///
|
|
func softDeleteCipherWithServer(id: String, _ cipher: Cipher) async throws
|
|
|
|
/// Updates the cipher's collections for the current user both in the backend and in local storage.
|
|
///
|
|
/// - Parameter cipher: The cipher to update.
|
|
///
|
|
func updateCipherCollectionsWithServer(_ cipher: Cipher) async throws
|
|
|
|
/// Updates the cipher for the current user both in the backend and in local storage.
|
|
///
|
|
/// - Parameters:
|
|
/// - cipher: The cipher to update.
|
|
/// - encryptedFor: The user ID who encrypted the `cipher`.
|
|
func updateCipherWithServer(_ cipher: Cipher, encryptedFor: String) async throws
|
|
|
|
/// Updates the cipher for the current user in local storage only.
|
|
///
|
|
/// - Parameter cipher: The cipher to update.
|
|
///
|
|
func updateCipherWithLocalStorage(_ cipher: Cipher) async throws
|
|
|
|
// MARK: Publishers
|
|
|
|
/// A publisher that emits individual cipher changes (insert, update, delete) as they occur for the current user.
|
|
///
|
|
/// This publisher only emits for individual cipher operations
|
|
/// Batch operations like `replaceCiphers` do not trigger emissions from this publisher.
|
|
///
|
|
/// - Returns: A publisher that emits cipher changes.
|
|
///
|
|
func cipherChangesPublisher() async throws -> AnyPublisher<CipherChange, Error>
|
|
|
|
/// A publisher for the list of ciphers for the current user.
|
|
///
|
|
/// - Returns: The list of encrypted ciphers.
|
|
///
|
|
func ciphersPublisher() async throws -> AnyPublisher<[Cipher], Error>
|
|
}
|
|
|
|
// MARK: - DefaultCipherService
|
|
|
|
class DefaultCipherService: CipherService {
|
|
// MARK: Properties
|
|
|
|
/// The service used to make cipher related API requests.
|
|
private let cipherAPIService: CipherAPIService
|
|
|
|
/// The data store for managing the persisted ciphers for the user.
|
|
private let cipherDataStore: CipherDataStore
|
|
|
|
/// The service used to make file related API requests.
|
|
private let fileAPIService: FileAPIService
|
|
|
|
/// The service used by the application to manage account state.
|
|
private let stateService: StateService
|
|
|
|
// MARK: Initialization
|
|
|
|
/// Initialize a `DefaultCipherService`.
|
|
///
|
|
/// - Parameters:
|
|
/// - cipherAPIService: The service used to make cipher related API requests.
|
|
/// - cipherDataStore: The data store for managing the persisted ciphers for the user.
|
|
/// - fileAPIService: The service used to make file related API requests.
|
|
/// - stateService: The service used by the application to manage account state.
|
|
///
|
|
init(
|
|
cipherAPIService: CipherAPIService,
|
|
cipherDataStore: CipherDataStore,
|
|
fileAPIService: FileAPIService,
|
|
stateService: StateService,
|
|
) {
|
|
self.cipherAPIService = cipherAPIService
|
|
self.cipherDataStore = cipherDataStore
|
|
self.fileAPIService = fileAPIService
|
|
self.stateService = stateService
|
|
}
|
|
}
|
|
|
|
extension DefaultCipherService {
|
|
func addCipherWithServer(_ cipher: Cipher, encryptedFor: String) async throws {
|
|
let userId = try await stateService.getActiveAccountId()
|
|
|
|
// Add the cipher in the backend.
|
|
var response: CipherDetailsResponseModel = if cipher.collectionIds.isEmpty {
|
|
try await cipherAPIService.addCipher(cipher, encryptedFor: encryptedFor)
|
|
} else {
|
|
try await cipherAPIService.addCipherWithCollections(cipher, encryptedFor: encryptedFor)
|
|
}
|
|
|
|
// The API doesn't return the collectionIds, so manually add them back.
|
|
response.collectionIds = cipher.collectionIds
|
|
|
|
// Add the cipher in local storage.
|
|
try await cipherDataStore.upsertCipher(Cipher(responseModel: response), userId: userId)
|
|
}
|
|
|
|
func cipherCount() async throws -> Int {
|
|
let userId = try await stateService.getActiveAccountId()
|
|
return try await cipherDataStore.cipherCount(userId: userId)
|
|
}
|
|
|
|
func deleteAttachmentWithServer(attachmentId: String, cipherId: String) async throws -> Cipher? {
|
|
let userId = try await stateService.getActiveAccountId()
|
|
|
|
// Delete attachment from the backend.
|
|
let response = try await cipherAPIService.deleteAttachment(withID: attachmentId, cipherId: cipherId)
|
|
|
|
// Remove the attachment from the cipher.
|
|
guard let cipher = try await cipherDataStore.fetchCipher(withId: cipherId, userId: userId) else { return nil }
|
|
var attachments = cipher.attachments ?? []
|
|
if let index = attachments.firstIndex(where: { $0.id == attachmentId }) {
|
|
attachments.remove(at: index)
|
|
}
|
|
let updatedCipher = cipher.update(
|
|
attachments: attachments,
|
|
revisionDate: response.cipher.revisionDate,
|
|
)
|
|
|
|
// Update the cipher in local storage.
|
|
try await cipherDataStore.upsertCipher(updatedCipher, userId: userId)
|
|
|
|
// Return the updated cipher.
|
|
return updatedCipher
|
|
}
|
|
|
|
func deleteCipherWithServer(id: String) async throws {
|
|
let userId = try await stateService.getActiveAccountId()
|
|
|
|
// Delete cipher from the backend.
|
|
_ = try await cipherAPIService.deleteCipher(withID: id)
|
|
|
|
// Delete cipher from local storage.
|
|
try await cipherDataStore.deleteCipher(id: id, userId: userId)
|
|
}
|
|
|
|
func deleteCipherWithLocalStorage(id: String) async throws {
|
|
let userId = try await stateService.getActiveAccountId()
|
|
try await cipherDataStore.deleteCipher(id: id, userId: userId)
|
|
}
|
|
|
|
func downloadAttachment(withId id: String, cipherId: String) async throws -> URL? {
|
|
// Get the url that contains the downloadable data for the attachment.
|
|
let response = try await cipherAPIService.downloadAttachment(withId: id, cipherId: cipherId)
|
|
|
|
// Download the data from the url.
|
|
return try await cipherAPIService.downloadAttachmentData(from: response.url)
|
|
}
|
|
|
|
func fetchCipher(withId id: String) async throws -> Cipher? {
|
|
let userId = try await stateService.getActiveAccountId()
|
|
return try await cipherDataStore.fetchCipher(withId: id, userId: userId)
|
|
}
|
|
|
|
func fetchAllCiphers() async throws -> [Cipher] {
|
|
let userId = try await stateService.getActiveAccountId()
|
|
return try await cipherDataStore.fetchAllCiphers(userId: userId)
|
|
}
|
|
|
|
func syncCipherWithServer(withId id: String) async throws {
|
|
let userId = try await stateService.getActiveAccountId()
|
|
let response = try await cipherAPIService.getCipher(withId: id)
|
|
let cipher = Cipher(responseModel: response)
|
|
try await cipherDataStore.upsertCipher(cipher, userId: userId)
|
|
}
|
|
|
|
func replaceCiphers(_ ciphers: [CipherDetailsResponseModel], userId: String) async throws {
|
|
try await cipherDataStore.replaceCiphers(ciphers.map(Cipher.init), userId: userId)
|
|
}
|
|
|
|
func restoreCipherWithServer(id: String, _ cipher: Cipher) async throws {
|
|
let userID = try await stateService.getActiveAccountId()
|
|
|
|
// Restore cipher from backend.
|
|
_ = try await cipherAPIService.restoreCipher(withID: id)
|
|
|
|
// Restore cipher from local storage
|
|
try await cipherDataStore.upsertCipher(cipher, userId: userID)
|
|
}
|
|
|
|
func saveAttachmentWithServer(cipher: Cipher, attachment: AttachmentEncryptResult) async throws -> Cipher {
|
|
guard let cipherId = cipher.id else { throw CipherAPIServiceError.updateMissingId }
|
|
|
|
let userId = try await stateService.getActiveAccountId()
|
|
|
|
// Create the cipher attachment in the backend
|
|
var response = try await cipherAPIService.saveAttachment(
|
|
cipherId: cipherId,
|
|
fileName: attachment.attachment.fileName,
|
|
fileSize: Int(attachment.attachment.size ?? ""),
|
|
key: attachment.attachment.key,
|
|
)
|
|
|
|
// Upload the attachment data to the server.
|
|
try await fileAPIService.uploadCipherAttachment(
|
|
attachmentId: response.attachmentId,
|
|
cipherId: response.cipherResponse.id,
|
|
data: attachment.contents,
|
|
fileName: attachment.attachment.fileName ?? "",
|
|
type: response.fileUploadType,
|
|
url: response.url,
|
|
)
|
|
|
|
// The API doesn't return the collectionIds, so manually add them back.
|
|
response.cipherResponse.collectionIds = cipher.collectionIds
|
|
|
|
// Update the cipher in local storage.
|
|
let updatedCipher = Cipher(responseModel: response.cipherResponse)
|
|
try await cipherDataStore.upsertCipher(updatedCipher, userId: userId)
|
|
|
|
// Return the updated cipher.
|
|
return updatedCipher
|
|
}
|
|
|
|
func shareCipherWithServer(_ cipher: Cipher, encryptedFor: String) async throws {
|
|
let userId = try await stateService.getActiveAccountId()
|
|
|
|
// Share the cipher from the backend.
|
|
var response = try await cipherAPIService.shareCipher(cipher, encryptedFor: encryptedFor)
|
|
|
|
// The API doesn't return the collectionIds, so manually add them back.
|
|
response.collectionIds = cipher.collectionIds
|
|
|
|
// Update the cipher in local storage.
|
|
try await cipherDataStore.upsertCipher(Cipher(responseModel: response), userId: userId)
|
|
}
|
|
|
|
func softDeleteCipherWithServer(id: String, _ cipher: BitwardenSdk.Cipher) async throws {
|
|
let userId = try await stateService.getActiveAccountId()
|
|
|
|
// Soft delete cipher from the backend.
|
|
_ = try await cipherAPIService.softDeleteCipher(withID: id)
|
|
|
|
// Soft delete cipher from local storage.
|
|
try await cipherDataStore.upsertCipher(cipher, userId: userId)
|
|
}
|
|
|
|
func updateCipherCollectionsWithServer(_ cipher: Cipher) async throws {
|
|
let userId = try await stateService.getActiveAccountId()
|
|
|
|
// Update the cipher collections in the backend.
|
|
try await cipherAPIService.updateCipherCollections(cipher)
|
|
|
|
// Update the cipher collections in local storage.
|
|
try await cipherDataStore.upsertCipher(cipher, userId: userId)
|
|
}
|
|
|
|
func updateCipherWithServer(_ cipher: Cipher, encryptedFor: String) async throws {
|
|
let userId = try await stateService.getActiveAccountId()
|
|
|
|
// Update the cipher in the backend.
|
|
var response: CipherDetailsResponseModel = if cipher.edit {
|
|
try await cipherAPIService.updateCipher(cipher, encryptedFor: encryptedFor)
|
|
} else {
|
|
// if the cipher is not editable, update the favorite status and folder only.
|
|
try await cipherAPIService.updateCipherPreference(cipher)
|
|
}
|
|
|
|
// The API doesn't return the collectionIds, so manually add them back.
|
|
response.collectionIds = cipher.collectionIds
|
|
|
|
// Update the cipher in local storage.
|
|
try await cipherDataStore.upsertCipher(Cipher(responseModel: response), userId: userId)
|
|
}
|
|
|
|
func updateCipherWithLocalStorage(_ cipher: Cipher) async throws {
|
|
let userId = try await stateService.getActiveAccountId()
|
|
try await cipherDataStore.upsertCipher(cipher, userId: userId)
|
|
}
|
|
|
|
// MARK: Publishers
|
|
|
|
func cipherChangesPublisher() async throws -> AnyPublisher<CipherChange, Error> {
|
|
let userId = try await stateService.getActiveAccountId()
|
|
return cipherDataStore.cipherChangesPublisher(userId: userId)
|
|
}
|
|
|
|
func ciphersPublisher() async throws -> AnyPublisher<[Cipher], Error> {
|
|
let userId = try await stateService.getActiveAccountId()
|
|
return cipherDataStore.cipherPublisher(userId: userId)
|
|
}
|
|
}
|