Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@ struct UserDecryptionResponseModel: Codable, Equatable {

/// The user's master password unlock info.
let masterPasswordUnlock: MasterPasswordUnlockResponseModel?

/// The hex-encoded ID of the user's current key.
///
/// - Note: `nil` for legacy V1 accounts whose keys carry no key ID.
let userKeyId: String?
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,12 @@ extension DefaultImportCiphersRepository: ImportCiphersRepository {
}

let encryptionContexts = try await clientService.exporters().importCxf(payload: accountJsonString)
let ciphers = encryptionContexts.map(\.cipher)

await onProgress(0.3)

_ = try await importCiphersService
.importCiphers(
ciphers: ciphers,
encryptionContexts: encryptionContexts,
folders: [],
folderRelationships: [],
)
Expand All @@ -104,7 +103,7 @@ extension DefaultImportCiphersRepository: ImportCiphersRepository {

try await syncService.fetchSync(forceSync: true)

let importedCredentialsCount = cxfCredentialsResultBuilder.build(from: ciphers)
let importedCredentialsCount = cxfCredentialsResultBuilder.build(from: encryptionContexts.map(\.cipher))

await onProgress(1.0)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ class ImportCiphersRepositoryTests: BitwardenTestCase {

XCTAssertNotNil(clientService.mockExporters.importCxfReceivedPayload)
XCTAssertTrue(importCiphersService.importCiphersCalled)
XCTAssertEqual(importCiphersService.importCiphersCiphers?.count, 9)
XCTAssertEqual(importCiphersService.importCiphersEncryptionContexts?.count, 9)
XCTAssertEqual(importCiphersService.importCiphersEncryptionContexts?[0].encryptedFor, "1")
XCTAssertTrue(syncService.didFetchSync)
XCTAssertTrue(syncService.fetchSyncForceSync == true)
XCTAssertEqual(progressReports, [0.3, 0.8, 1.0])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,27 @@ import Networking
protocol ImportCiphersAPIService {
/// Performs an API request to import ciphers in the vault.
/// - Parameters:
/// - ciphers: The ciphers to import.
/// - encryptionContexts: The encryption contexts containing ciphers and their encryption metadata to import.
/// - folders: The folders to import.
/// - folderRelationships: The cipher<->folder relationships map. The key is the cipher index
/// and the value is the folder index in their respective arrays.
func importCiphers(
ciphers: [Cipher],
encryptionContexts: [EncryptionContext],
folders: [Folder],
folderRelationships: [(key: Int, value: Int)],
) async throws -> EmptyResponse
}

extension APIService: ImportCiphersAPIService {
func importCiphers(
ciphers: [Cipher],
encryptionContexts: [EncryptionContext],
folders: [Folder],
folderRelationships: [(key: Int, value: Int)],
) async throws -> EmptyResponse {
try await apiService
.send(
ImportCiphersRequest(
ciphers: ciphers,
encryptionContexts: encryptionContexts,
folders: folders,
folderRelationships: folderRelationships,
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import BitwardenSdk
import TestHelpers
import XCTest

Expand Down Expand Up @@ -28,27 +29,35 @@ class ImportCiphersAPIServiceTests: BitwardenTestCase {

// MARK: Tests

/// `importCiphers(ciphers:folders:folderRelationships:)` performs the import ciphers request.
/// `importCiphers(encryptionContexts:folders:folderRelationships:)` performs the import ciphers request.
func test_importCiphers() async throws {
client.results = [
.httpSuccess(testData: .emptyResponse),
]
_ = try await subject.importCiphers(ciphers: [.fixture()], folders: [], folderRelationships: [])
_ = try await subject.importCiphers(
encryptionContexts: [EncryptionContext(encryptedFor: "user-1", cipher: .fixture())],
folders: [],
folderRelationships: [],
)

XCTAssertEqual(client.requests.count, 1)
XCTAssertNotNil(client.requests[0].body)
XCTAssertEqual(client.requests[0].method, .post)
XCTAssertEqual(client.requests[0].url.absoluteString, "https://example.com/api/ciphers/import")
}

/// `importCiphers(ciphers:folders:folderRelationships:)` performs the import ciphers request.
/// `importCiphers(encryptionContexts:folders:folderRelationships:)` throws on API failure.
func test_importCiphers_throws() async throws {
client.results = [
.httpFailure(BitwardenTestError.example),
]

await assertAsyncThrows(error: BitwardenTestError.example) {
_ = try await subject.importCiphers(ciphers: [.fixture()], folders: [], folderRelationships: [])
_ = try await subject.importCiphers(
encryptionContexts: [EncryptionContext(encryptedFor: "user-1", cipher: .fixture())],
folders: [],
folderRelationships: [],
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,21 @@ struct ImportCiphersRequest: Request {

/// Initialize a `ImportCiphersRequest` for ciphers, folders and its relattionship.
/// - Parameters:
/// - ciphers: Ciphers to import.
/// - encryptionContexts: The encryption contexts containing ciphers and their encryption metadata to import.
/// - folders: Folders to import.
/// - folderRelationships: The cipher<->folder relationships map. The key is the cipher index
/// and the value is the folder index in their respective arrays.
init(
ciphers: [Cipher],
encryptionContexts: [EncryptionContext],
folders: [Folder] = [],
folderRelationships: [(key: Int, value: Int)] = [],
) throws {
guard !ciphers.isEmpty else {
guard !encryptionContexts.isEmpty else {
throw BitwardenError.dataError("There are no ciphers to import.")
}

requestModel = ImportCiphersRequestModel(
ciphers: ciphers.map { CipherRequestModel(cipher: $0) },
ciphers: encryptionContexts.map { CipherRequestModel(encryptionContext: $0) },
folders: folders.map { FolderWithIdRequestModel(folder: $0) },
folderRelationships: folderRelationships.map { FolderRelationship(key: $0.key, value: $0.value) },
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import BitwardenSdk
import XCTest

@testable import BitwardenShared
Expand All @@ -8,35 +9,51 @@ import XCTest
class ImportCiphersRequestTests: BitwardenTestCase {
// MARK: Tests

/// `init(ciphers:folders:folderRelationships:)` initializes the request successfully.
/// `init(encryptionContexts:folders:folderRelationships:)` initializes the request successfully.
func test_init() throws {
let subject = try ImportCiphersRequest(
ciphers: [.fixture(name: "cipherTest")],
encryptionContexts: [
EncryptionContext(
encryptedFor: "user-1",
encryptedByKeyId: "key-abc",
cipher: .fixture(name: "cipherTest"),
),
],
folders: [.fixture(name: "folderTest")],
folderRelationships: [(1, 1)],
)
XCTAssertEqual(subject.body?.ciphers[0].name, "cipherTest")
XCTAssertEqual(subject.body?.ciphers[0].encryptedFor, "user-1")
XCTAssertEqual(subject.body?.ciphers[0].encryptedByKeyId, "key-abc")
XCTAssertEqual(subject.body?.folders[0].name, "folderTest")
XCTAssertEqual(subject.body?.folderRelationships[0].key, 1)
XCTAssertEqual(subject.body?.folderRelationships[0].value, 1)
}

/// `init(ciphers:folders:folderRelationships:)` initializes the request successfully.
/// `init(encryptionContexts:folders:folderRelationships:)` throws when the contexts are empty.
func test_init_throws() throws {
XCTAssertThrowsError(_ = try ImportCiphersRequest(
ciphers: [],
encryptionContexts: [],
))
}

/// `path` returns the correct path.
func test_path() throws {
let subject = try ImportCiphersRequest(ciphers: [.fixture()])
let subject = try ImportCiphersRequest(
encryptionContexts: [
EncryptionContext(encryptedFor: "user-1", cipher: .fixture()),
],
)
XCTAssertEqual(subject.path, "/ciphers/import")
}

/// `method` is `.put`.
/// `method` is `.post`.
func test_method() throws {
let subject = try ImportCiphersRequest(ciphers: [.fixture()])
let subject = try ImportCiphersRequest(
encryptionContexts: [
EncryptionContext(encryptedFor: "user-1", cipher: .fixture()),
],
)
XCTAssertEqual(subject.method, .post)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import Foundation
protocol ImportCiphersService {
/// Performs an API request to import ciphers in the vault.
/// - Parameters:
/// - ciphers: The ciphers to import.
/// - encryptionContexts: The encryption contexts containing ciphers and their encryption metadata to import.
/// - folders: The folders to import.
/// - folderRelationships: The cipher<->folder relationships map. The key is the cipher index
/// and the value is the folder index in their respective arrays.
func importCiphers(
ciphers: [Cipher],
encryptionContexts: [EncryptionContext],
folders: [Folder],
folderRelationships: [(key: Int, value: Int)],
) async throws
Expand Down Expand Up @@ -42,13 +42,13 @@ class DefaultImportCiphersService: ImportCiphersService {

extension DefaultImportCiphersService {
func importCiphers(
ciphers: [Cipher],
encryptionContexts: [EncryptionContext],
folders: [Folder],
folderRelationships: [(key: Int, value: Int)],
) async throws {
_ = try await importCiphersAPIService
.importCiphers(
ciphers: ciphers,
encryptionContexts: encryptionContexts,
folders: folders,
folderRelationships: folderRelationships,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import BitwardenSdk
import TestHelpers
import XCTest

Expand Down Expand Up @@ -33,20 +34,28 @@ class ImportCiphersServiceTests: BitwardenTestCase {

// MARK: Tests

/// `importCiphers(ciphers:folders:folderRelationships:)` import the ciphers calling the API.
/// `importCiphers(encryptionContexts:folders:folderRelationships:)` imports the ciphers calling the API.
func test_importCiphers_succeeds() async throws {
client.results = [.httpSuccess(testData: .emptyResponse)]
try await subject.importCiphers(ciphers: [.fixture()], folders: [], folderRelationships: [])
try await subject.importCiphers(
encryptionContexts: [EncryptionContext(encryptedFor: "user-1", cipher: .fixture())],
folders: [],
folderRelationships: [],
)
let request = try XCTUnwrap(client.requests.first)
XCTAssertEqual(request.url.absoluteString, "https://example.com/api/ciphers/import")
XCTAssertEqual(request.method, .post)
}

/// `importCiphers(ciphers:folders:folderRelationships:)` throws when calling the API.
/// `importCiphers(encryptionContexts:folders:folderRelationships:)` throws when calling the API.
func test_importCiphers_throws() async throws {
client.results = [.httpFailure(BitwardenTestError.example)]
await assertAsyncThrows(error: BitwardenTestError.example) {
try await subject.importCiphers(ciphers: [.fixture()], folders: [], folderRelationships: [])
try await subject.importCiphers(
encryptionContexts: [EncryptionContext(encryptedFor: "user-1", cipher: .fixture())],
folders: [],
folderRelationships: [],
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@ import BitwardenSdk

class MockImportCiphersService: ImportCiphersService {
var importCiphersCalled = false
var importCiphersCiphers: [Cipher]?
var importCiphersEncryptionContexts: [EncryptionContext]?
var importCiphersError: Error?
var importCiphersFolders: [Folder]?
var importCiphersFolderRelationships: [(key: Int, value: Int)]?
var importCiphersFolders: [Folder]?

func importCiphers(
ciphers: [Cipher],
encryptionContexts: [EncryptionContext],
folders: [Folder],
folderRelationships: [(key: Int, value: Int)],
) async throws {
importCiphersCalled = true
importCiphersCiphers = ciphers
importCiphersFolders = folders
importCiphersEncryptionContexts = encryptionContexts
importCiphersFolderRelationships = folderRelationships
importCiphersFolders = folders
if let importCiphersError {
throw importCiphersError
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class DefaultCipherEncryptionMediator: CipherEncryptionMediator {
if didAddCipherKey {
try await cipherService.updateCipherWithServer(
cipherEncryptionContext.cipher,
encryptedByKeyId: cipherEncryptionContext.encryptedByKeyId,
encryptedFor: cipherEncryptionContext.encryptedFor,
)
}
Expand All @@ -89,6 +90,7 @@ class DefaultCipherEncryptionMediator: CipherEncryptionMediator {

try await cipherService.updateCipherWithServer(
cipherEncryptionContext.cipher,
encryptedByKeyId: cipherEncryptionContext.encryptedByKeyId,
encryptedFor: cipherEncryptionContext.encryptedFor,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@ class CipherEncryptionMediatorTests: BitwardenTestCase {
let cipherView = CipherView.fixture(key: nil)
let encryptedCipher = Cipher.fixture(key: "encryptedKey")
clientService.mockVault.clientCiphers.encryptClosure = { _ in
EncryptionContext(encryptedFor: "userId", cipher: encryptedCipher)
EncryptionContext(encryptedFor: "userId", encryptedByKeyId: "key-1", cipher: encryptedCipher)
}

let result = try await subject.encryptAndUpdateCipher(cipherView)

XCTAssertEqual(cipherService.updateCipherWithServerCiphers, [encryptedCipher])
XCTAssertEqual(cipherService.updateCipherWithServerEncryptedByKeyId, "key-1")
XCTAssertEqual(cipherService.updateCipherWithServerEncryptedFor, "userId")
XCTAssertEqual(result, encryptedCipher)
}
Expand Down Expand Up @@ -155,14 +156,15 @@ class CipherEncryptionMediatorTests: BitwardenTestCase {
let encryptedCipher = Cipher.fixture(key: "encryptedKey")
let updatedCipherView = CipherView.fixture(id: "1", key: "decryptedKey")
clientService.mockVault.clientCiphers.encryptClosure = { _ in
EncryptionContext(encryptedFor: "userId", cipher: encryptedCipher)
EncryptionContext(encryptedFor: "userId", encryptedByKeyId: "key-1", cipher: encryptedCipher)
}
delegate.fetchCipherReturnValue = updatedCipherView
subject.setDelegate(delegate)

let result = try await subject.updateCipherKeyIfNeeded(cipherView)

XCTAssertEqual(cipherService.updateCipherWithServerCiphers, [encryptedCipher])
XCTAssertEqual(cipherService.updateCipherWithServerEncryptedByKeyId, "key-1")
XCTAssertEqual(cipherService.updateCipherWithServerEncryptedFor, "userId")
XCTAssertEqual(delegate.fetchCipherReceivedId, "1")
XCTAssertEqual(result, updatedCipherView)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,21 @@ struct BulkShareCiphersRequestModel: JSONRequestBody {
}

extension BulkShareCiphersRequestModel {
/// Initialize a `BulkShareCiphersRequestModel` from an array of `Cipher` objects.
/// Initialize a `BulkShareCiphersRequestModel` from an array of `EncryptionContext` objects.
///
/// - Parameters:
/// - ciphers: The `Cipher` objects to share.
/// - encryptionContexts: The encryption contexts containing the ciphers and per-cipher encryption metadata.
/// - collectionIds: The collection identifiers to share the ciphers with.
/// - encryptedFor: The user ID who encrypted the ciphers.
///
init(ciphers: [Cipher], collectionIds: [String], encryptedFor: String?) {
self.ciphers = ciphers.map { CipherRequestModel(cipher: $0, encryptedFor: encryptedFor, includeId: true) }
init(encryptionContexts: [EncryptionContext], collectionIds: [String]) {
ciphers = encryptionContexts.map { context in
CipherRequestModel(
cipher: context.cipher,
encryptedByKeyId: context.encryptedByKeyId,
encryptedFor: context.encryptedFor,
includeId: true,
)
}
self.collectionIds = collectionIds
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ extension CipherCreateRequestModel {
///
/// - Parameters:
/// - cipher: The `Cipher` used to initialize a `CipherCreateRequestModel`.
/// - encryptedByKeyId: The hex-encoded ID of the key used to encrypt the `cipher`.
/// - encryptedFor: The user ID who encrypted the `cipher`.
init(cipher: Cipher, encryptedFor: String?) {
self.cipher = CipherRequestModel(cipher: cipher, encryptedFor: encryptedFor)
init(cipher: Cipher, encryptedByKeyId: String? = nil, encryptedFor: String?) {
self.cipher = CipherRequestModel(cipher: cipher, encryptedByKeyId: encryptedByKeyId, encryptedFor: encryptedFor)
collectionIds = cipher.collectionIds
}
}
Loading
Loading