mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-11 04:34:55 -06:00
[PM-14800] Credential Exchange implementation preparation (#1198)
This commit is contained in:
parent
970f698e0d
commit
58260a9b95
@ -1,3 +1,4 @@
|
||||
import AuthenticationServices
|
||||
import BitwardenShared
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
@ -78,13 +79,27 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
_ scene: UIScene,
|
||||
continue userActivity: NSUserActivity
|
||||
) {
|
||||
guard
|
||||
let appProcessor,
|
||||
userActivity.activityType == NSUserActivityTypeBrowsingWeb,
|
||||
let incomingURL = userActivity.webpageURL
|
||||
else { return }
|
||||
guard let appProcessor else {
|
||||
return
|
||||
}
|
||||
|
||||
appProcessor.handleAppLinks(incomingURL: incomingURL)
|
||||
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
|
||||
let incomingURL = userActivity.webpageURL {
|
||||
appProcessor.handleAppLinks(incomingURL: incomingURL)
|
||||
}
|
||||
|
||||
#if compiler(>=6.0.3)
|
||||
|
||||
if #available(iOS 18.2, *),
|
||||
userActivity.activityType == ASCredentialExchangeActivity {
|
||||
guard let token = userActivity.userInfo?[ASCredentialImportToken] as? UUID else {
|
||||
return
|
||||
}
|
||||
|
||||
appProcessor.handleImportCredentials(credentialImportToken: token)
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
func scene(_ scene: UIScene, openURLContexts urlContexts: Set<UIOpenURLContext>) {
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
<dict>
|
||||
<key>BitwardenAppIdentifier</key>
|
||||
<string>$(BASE_BUNDLE_ID)</string>
|
||||
<key>BitwardenAuthenticatorSharedAppGroup</key>
|
||||
<string>${SHARED_APP_GROUP_IDENTIFIER}</string>
|
||||
<key>BitwardenKeychainAccessGroup</key>
|
||||
<string>$(AppIdentifierPrefix)$(BASE_BUNDLE_ID)</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
@ -150,7 +152,5 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Resources/Assets.xcassets/AppIcons.appiconset</string>
|
||||
<key>BitwardenAuthenticatorSharedAppGroup</key>
|
||||
<string>${SHARED_APP_GROUP_IDENTIFIER}</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -6,18 +6,18 @@
|
||||
<string>$(BASE_BUNDLE_ID)</string>
|
||||
<key>BitwardenKeychainAccessGroup</key>
|
||||
<string>$(AppIdentifierPrefix)$(BASE_BUNDLE_ID)</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Bitwarden</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Bitwarden Autofill</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
@ -56,37 +56,24 @@
|
||||
<string>el</string>
|
||||
<string>th</string>
|
||||
</array>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Bitwarden Autofill</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<dict>
|
||||
<key>arm64</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<true/>
|
||||
<key>ITSEncryptionExportComplianceCode</key>
|
||||
<string>ecf076d3-4824-4d7b-b716-2a9a47d7d296</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Use Face ID to unlock your vault.</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionMainStoryboard</key>
|
||||
<string>MainInterface</string>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.authentication-services-credential-provider-ui</string>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>ASCredentialProviderExtensionShowsConfigurationUI</key>
|
||||
<true/>
|
||||
<key>ASCredentialProviderExtensionCapabilities</key>
|
||||
<dict>
|
||||
<key>ProvidesOneTimeCodes</key>
|
||||
@ -98,7 +85,20 @@
|
||||
<key>ShowsConfigurationUI</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>ASCredentialProviderExtensionShowsConfigurationUI</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSExtensionMainStoryboard</key>
|
||||
<string>MainInterface</string>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.authentication-services-credential-provider-ui</string>
|
||||
</dict>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Use Face ID to unlock your vault.</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<dict>
|
||||
<key>arm64</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -1056,7 +1056,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
|
||||
|
||||
/// `isLocked` returns the lock state of an active user.
|
||||
func test_isLocked_noHistory() async throws {
|
||||
let account: Account = .fixture()
|
||||
let account: BitwardenShared.Account = .fixture()
|
||||
stateService.activeAccount = account
|
||||
vaultTimeoutService.isClientLocked[account.profile.userId] = true
|
||||
let isLocked = try await subject.isLocked()
|
||||
|
||||
@ -4,6 +4,12 @@ import Foundation
|
||||
|
||||
/// An enum to represent a feature flag sent by the server
|
||||
enum FeatureFlag: String, CaseIterable, Codable {
|
||||
/// Flag to enable/disable Credential Exchange export flow.
|
||||
case cxpExportMobile = "cxp-export-mobile"
|
||||
|
||||
/// Flag to enable/disable Credential Exchange import flow.
|
||||
case cxpImportMobile = "cxp-import-mobile"
|
||||
|
||||
/// Flag to enable/disable email verification during registration
|
||||
/// This flag introduces a new flow for account creation
|
||||
case emailVerification = "email-verification"
|
||||
@ -89,7 +95,9 @@ enum FeatureFlag: String, CaseIterable, Codable {
|
||||
.testLocalInitialIntFlag,
|
||||
.testLocalInitialStringFlag:
|
||||
false
|
||||
case .emailVerification,
|
||||
case .cxpExportMobile,
|
||||
.cxpImportMobile,
|
||||
.emailVerification,
|
||||
.enableAuthenticatorSync,
|
||||
.refactorSsoDetailsEndpoint,
|
||||
.sshKeyVaultItem,
|
||||
|
||||
@ -14,6 +14,8 @@ final class FeatureFlagTests: BitwardenTestCase {
|
||||
|
||||
/// `getter:isRemotelyConfigured` returns the correct value for each flag.
|
||||
func test_isRemotelyConfigured() {
|
||||
XCTAssertTrue(FeatureFlag.cxpExportMobile.isRemotelyConfigured)
|
||||
XCTAssertTrue(FeatureFlag.cxpImportMobile.isRemotelyConfigured)
|
||||
XCTAssertTrue(FeatureFlag.emailVerification.isRemotelyConfigured)
|
||||
XCTAssertTrue(FeatureFlag.enableAuthenticatorSync.isRemotelyConfigured)
|
||||
XCTAssertTrue(FeatureFlag.refactorSsoDetailsEndpoint.isRemotelyConfigured)
|
||||
|
||||
@ -58,19 +58,7 @@ extension JSONDecoder {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .custom { keys in
|
||||
let key = keys.last!.stringValue
|
||||
let camelCaseKey: String
|
||||
if key.contains("_") {
|
||||
// Handle snake_case.
|
||||
camelCaseKey = key.lowercased()
|
||||
.split(separator: "_")
|
||||
.enumerated()
|
||||
.map { $0.offset > 0 ? $0.element.capitalized : $0.element.lowercased() }
|
||||
.joined()
|
||||
} else {
|
||||
// Handle PascalCase or camelCase.
|
||||
camelCaseKey = key.prefix(1).lowercased() + key.dropFirst()
|
||||
}
|
||||
return AnyKey(stringValue: camelCaseKey)
|
||||
return AnyKey(stringValue: keyToCamelCase(key: key))
|
||||
}
|
||||
decoder.dateDecodingStrategy = defaultDecoder.dateDecodingStrategy
|
||||
return decoder
|
||||
@ -83,4 +71,47 @@ extension JSONDecoder {
|
||||
decoder.dateDecodingStrategy = defaultDecoder.dateDecodingStrategy
|
||||
return decoder
|
||||
}()
|
||||
|
||||
/// A `JSONDecoder` instance that handles decoding JSON from CXP format to Apple's expected format.
|
||||
static let cxpDecoder: JSONDecoder = {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .custom { keys in
|
||||
let key = keys.last!.stringValue
|
||||
let camelCaseKey = keyToCamelCase(key: key)
|
||||
return AnyKey(stringValue: customTransformCodingKeyForCXP(key: camelCaseKey))
|
||||
}
|
||||
decoder.dateDecodingStrategy = .secondsSince1970
|
||||
return decoder
|
||||
}()
|
||||
|
||||
// MARK: Static Functions
|
||||
|
||||
/// Transforms a snake_case, PascalCase or camelCase key into camelCase.
|
||||
static func keyToCamelCase(key: String) -> String {
|
||||
if key.contains("_") {
|
||||
// Handle snake_case.
|
||||
return key.lowercased()
|
||||
.split(separator: "_")
|
||||
.enumerated()
|
||||
.map { $0.offset > 0 ? $0.element.capitalized : $0.element.lowercased() }
|
||||
.joined()
|
||||
}
|
||||
|
||||
// Handle PascalCase or camelCase.
|
||||
return key.prefix(1).lowercased() + key.dropFirst()
|
||||
}
|
||||
|
||||
// MARK: Private Static Functions
|
||||
|
||||
/// Transforms the keys from CXP format handled by the Bitwarden SDK into the keys that Apple expects.
|
||||
private static func customTransformCodingKeyForCXP(key: String) -> String {
|
||||
return switch key {
|
||||
case "credentialId":
|
||||
"credentialID"
|
||||
case "rpId":
|
||||
"rpID"
|
||||
default:
|
||||
key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,15 +6,8 @@ struct APITestData {
|
||||
let data: Data
|
||||
|
||||
static func loadFromBundle(resource: String, extension: String) -> APITestData {
|
||||
let bundle = Bundle(for: BitwardenTestCase.self)
|
||||
guard let url = bundle.url(forResource: resource, withExtension: `extension`) else {
|
||||
fatalError("Unable to locate file \(resource).\(`extension`) in the bundle.")
|
||||
}
|
||||
do {
|
||||
return try APITestData(data: Data(contentsOf: url))
|
||||
} catch {
|
||||
fatalError("Unable to load data from \(resource).\(`extension`) in the bundle. Error: \(error)")
|
||||
}
|
||||
let data = TestDataHelpers.loadFromBundle(resource: resource, extension: `extension`)
|
||||
return APITestData(data: data)
|
||||
}
|
||||
|
||||
static func loadFromJsonBundle(resource: String) -> APITestData {
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
import Foundation
|
||||
|
||||
/// A type that wraps fixture data for use in mocking responses during tests.
|
||||
///
|
||||
enum TestDataHelpers {
|
||||
static func loadFromBundle(resource: String, extension: String) -> Data {
|
||||
let bundle = Bundle(for: BitwardenTestCase.self)
|
||||
guard let url = bundle.url(forResource: resource, withExtension: `extension`) else {
|
||||
fatalError("Unable to locate file \(resource).\(`extension`) in the bundle.")
|
||||
}
|
||||
do {
|
||||
return try Data(contentsOf: url)
|
||||
} catch {
|
||||
fatalError("Unable to load data from \(resource).\(`extension`) in the bundle. Error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
static func loadFromJsonBundle(resource: String) -> Data {
|
||||
loadFromBundle(resource: resource, extension: "json")
|
||||
}
|
||||
|
||||
static func loadUTFStringFromJsonBundle(resource: String) -> String? {
|
||||
let data = loadFromJsonBundle(resource: resource)
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
@ -28,7 +28,7 @@ protocol ClientService {
|
||||
/// - Parameter userId: The user ID mapped to the client instance.
|
||||
/// - Returns: A `ClientExportersProtocol` for vault export data tasks.
|
||||
///
|
||||
func exporters(for userId: String?) async throws -> ClientExportersProtocol
|
||||
func exporters(for userId: String?) async throws -> ClientExportersServiceTemp
|
||||
|
||||
/// Returns a `ClientGeneratorsProtocol` for generator data tasks.
|
||||
///
|
||||
@ -88,7 +88,7 @@ extension ClientService {
|
||||
|
||||
/// Returns a `ClientExportersProtocol` for vault export data tasks.
|
||||
///
|
||||
func exporters() async throws -> ClientExportersProtocol {
|
||||
func exporters() async throws -> ClientExportersServiceTemp {
|
||||
try await exporters(for: nil)
|
||||
}
|
||||
|
||||
@ -199,7 +199,7 @@ actor DefaultClientService: ClientService {
|
||||
try await client(for: userId).crypto()
|
||||
}
|
||||
|
||||
func exporters(for userId: String?) async throws -> ClientExportersProtocol {
|
||||
func exporters(for userId: String?) async throws -> ClientExportersServiceTemp {
|
||||
try await client(for: userId).exporters()
|
||||
}
|
||||
|
||||
@ -360,7 +360,7 @@ protocol BitwardenSdkClient {
|
||||
func crypto() -> ClientCryptoProtocol
|
||||
|
||||
/// Returns exporters.
|
||||
func exporters() -> ClientExportersProtocol
|
||||
func exporters() -> ClientExportersServiceTemp
|
||||
|
||||
/// Returns generator operations.
|
||||
func generators() -> ClientGeneratorsProtocol
|
||||
@ -386,7 +386,7 @@ extension Client: BitwardenSdkClient {
|
||||
crypto() as ClientCrypto
|
||||
}
|
||||
|
||||
func exporters() -> ClientExportersProtocol {
|
||||
func exporters() -> ClientExportersServiceTemp {
|
||||
exporters() as ClientExporters
|
||||
}
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ class MockClient: BitwardenSdkClient {
|
||||
""
|
||||
}
|
||||
|
||||
func exporters() -> any ClientExportersProtocol {
|
||||
func exporters() -> any ClientExportersServiceTemp {
|
||||
clientExporters
|
||||
}
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@ class MockClientService: ClientService {
|
||||
mockCrypto
|
||||
}
|
||||
|
||||
func exporters(for userId: String?) -> ClientExportersProtocol {
|
||||
func exporters(for userId: String?) -> ClientExportersServiceTemp {
|
||||
mockExporters
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
import BitwardenSdk
|
||||
|
||||
/// API model for a request of a folder with id.
|
||||
///
|
||||
struct FolderWithIdRequestModel: Codable, Equatable {
|
||||
// MARK: Properties
|
||||
|
||||
/// A identifier for the folder.
|
||||
let id: String?
|
||||
|
||||
/// The name of the folder.
|
||||
let name: String?
|
||||
|
||||
/// Inits a `FolderWithIdRequestModel` from a `Folder`
|
||||
/// - Parameter folder: Folder from which initialize this request model.
|
||||
init(folder: Folder) {
|
||||
id = folder.id
|
||||
name = folder.name
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
import Foundation
|
||||
import Networking
|
||||
|
||||
// MARK: - ImportCiphersRequestModel
|
||||
|
||||
/// API request model for importing ciphers.
|
||||
///
|
||||
struct ImportCiphersRequestModel: JSONRequestBody {
|
||||
// MARK: Properties
|
||||
|
||||
/// The cipher request models to import.
|
||||
var ciphers: [CipherRequestModel]
|
||||
|
||||
/// The folders request models to import.
|
||||
var folders: [FolderWithIdRequestModel]
|
||||
|
||||
/// The cipher<->folder relationships map. The key is the cipher index and the value is the folder index
|
||||
/// in their respective arrays.
|
||||
var folderRelationships: [FolderRelationship]
|
||||
}
|
||||
|
||||
/// The cipher<->folder relationships map. The key is the cipher index and the value is the folder index
|
||||
/// in their respective arrays.
|
||||
struct FolderRelationship: Codable {
|
||||
/// The key of the relationship which refers to the cipher index
|
||||
/// of the `ciphers` array in `ImportCiphersRequestModel`.
|
||||
let key: Int
|
||||
|
||||
/// The value of the relationship which refers to the folder index
|
||||
/// of the `folders` array in `ImportCiphersRequestModel`.
|
||||
let value: Int
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import BitwardenSdk
|
||||
import Networking
|
||||
|
||||
// MARK: - ImportCiphersAPIService
|
||||
|
||||
/// A protocol for an API service used to make import ciphers requests.
|
||||
///
|
||||
protocol ImportCiphersAPIService {
|
||||
/// Performs an API request to import ciphers in the vault.
|
||||
/// - Parameters:
|
||||
/// - ciphers: The ciphers 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],
|
||||
folders: [Folder],
|
||||
folderRelationships: [(key: Int, value: Int)]
|
||||
) async throws -> EmptyResponse
|
||||
}
|
||||
|
||||
extension APIService: ImportCiphersAPIService {
|
||||
func importCiphers(
|
||||
ciphers: [Cipher],
|
||||
folders: [Folder],
|
||||
folderRelationships: [(key: Int, value: Int)]
|
||||
) async throws -> EmptyResponse {
|
||||
try await apiService
|
||||
.send(
|
||||
ImportCiphersRequest(
|
||||
ciphers: ciphers,
|
||||
folders: folders,
|
||||
folderRelationships: folderRelationships
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
|
||||
// MARK: - ImportCiphersAPIServiceTests
|
||||
|
||||
class ImportCiphersAPIServiceTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var client: MockHTTPClient!
|
||||
var subject: APIService!
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
client = MockHTTPClient()
|
||||
subject = APIService(client: client)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
client = nil
|
||||
subject = nil
|
||||
}
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// `importCiphers(ciphers: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: [])
|
||||
|
||||
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.
|
||||
func test_importCiphers_throws() async throws {
|
||||
client.results = [
|
||||
.httpFailure(BitwardenTestError.example),
|
||||
]
|
||||
|
||||
await assertAsyncThrows(error: BitwardenTestError.example) {
|
||||
_ = try await subject.importCiphers(ciphers: [.fixture()], folders: [], folderRelationships: [])
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
import BitwardenSdk
|
||||
import Networking
|
||||
|
||||
/// A request model for importing ciphers.
|
||||
///
|
||||
struct ImportCiphersRequest: Request {
|
||||
typealias Response = EmptyResponse
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The body of the request.
|
||||
var body: ImportCiphersRequestModel? {
|
||||
requestModel
|
||||
}
|
||||
|
||||
/// The HTTP method for this request.
|
||||
let method = HTTPMethod.post
|
||||
|
||||
/// The URL path for this request.
|
||||
let path = "/ciphers/import"
|
||||
|
||||
/// The request details to include in the body of the request.
|
||||
let requestModel: ImportCiphersRequestModel
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initialize a `ImportCiphersRequest` for ciphers, folders and its relattionship.
|
||||
/// - Parameters:
|
||||
/// - ciphers: Ciphers 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],
|
||||
folders: [Folder] = [],
|
||||
folderRelationships: [(key: Int, value: Int)] = []
|
||||
) throws {
|
||||
guard !ciphers.isEmpty else {
|
||||
throw BitwardenError.dataError("There are no ciphers to import.")
|
||||
}
|
||||
|
||||
requestModel = ImportCiphersRequestModel(
|
||||
ciphers: ciphers.map { CipherRequestModel(cipher: $0) },
|
||||
folders: folders.map { FolderWithIdRequestModel(folder: $0) },
|
||||
folderRelationships: folderRelationships.map { FolderRelationship(key: $0.key, value: $0.value) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
|
||||
// MARK: - ImportCiphersRequestTests
|
||||
|
||||
class ImportCiphersRequestTests: BitwardenTestCase {
|
||||
// MARK: Tests
|
||||
|
||||
/// `init(ciphers:folders:folderRelationships:)` initializes the request successfully.
|
||||
func test_init() throws {
|
||||
let subject = try ImportCiphersRequest(
|
||||
ciphers: [.fixture(name: "cipherTest")],
|
||||
folders: [.fixture(name: "folderTest")],
|
||||
folderRelationships: [(1, 1)]
|
||||
)
|
||||
XCTAssertEqual(subject.body?.ciphers[0].name, "cipherTest")
|
||||
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.
|
||||
func test_init_throws() throws {
|
||||
XCTAssertThrowsError(_ = try ImportCiphersRequest(
|
||||
ciphers: []
|
||||
))
|
||||
}
|
||||
|
||||
/// `path` returns the correct path.
|
||||
func test_path() throws {
|
||||
let subject = try ImportCiphersRequest(ciphers: [.fixture()])
|
||||
XCTAssertEqual(subject.path, "/ciphers/import")
|
||||
}
|
||||
|
||||
/// `method` is `.put`.
|
||||
func test_method() throws {
|
||||
let subject = try ImportCiphersRequest(ciphers: [.fixture()])
|
||||
XCTAssertEqual(subject.method, .post)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
// swiftlint:disable:this file_name
|
||||
|
||||
import BitwardenSdk
|
||||
|
||||
/// Temporary protocol of `ClientExportersProtocol` until the SDK PR gets merged and is available for CI
|
||||
/// https://github.com/bitwarden/sdk-internal/pull/32
|
||||
protocol ClientExportersServiceTemp: AnyObject {
|
||||
/// Exports ciphers with an account in Credential Exchange flow.
|
||||
func exportCxf(account: BitwardenSdkAccount, ciphers: [BitwardenSdk.Cipher]) throws -> String
|
||||
|
||||
/// Exports organization vault in a given format.
|
||||
func exportOrganizationVault(collections: [Collection], ciphers: [Cipher], format: ExportFormat) throws -> String
|
||||
|
||||
/// Exports vault with a given format.
|
||||
func exportVault(folders: [Folder], ciphers: [Cipher], format: ExportFormat) throws -> String
|
||||
|
||||
/// Imports ciphers in Credential Exchange flow.
|
||||
func importCxf(payload: String) throws -> [BitwardenSdk.Cipher]
|
||||
}
|
||||
|
||||
/// Mocking the responses of the export CXP flow until the SDK PR gets merged.
|
||||
extension ClientExporters: ClientExportersServiceTemp {
|
||||
func exportCxf(account: BitwardenSdkAccount, ciphers: [BitwardenSdk.Cipher]) throws -> String {
|
||||
"""
|
||||
{"items":[{"modifiedAt":1732226366,"creationAt":1732226366,"title":"GitHub","credentials":[{"urls":["github.com"],"password":{"id":"RTEzRDEwQjctRTdCQy00QTI3LTgwNDAtRjgxMzNBOTMxMjhC","fieldType":"concealed-string","value":"adsfasf"},"type":"basic-auth","username":{"fieldType":"string","id":"NTlBMUFBNUYtODE5My00QUIzLThGRjYtOEFCRUQ5MUQxNUZG","value":"TestCXP1"}}],"id":"MjZDQzQwQTQtQUZDQS00NEIzLUEwNjAtMUMyNUUzNTc1RTZB","type":"login"},{"type":"login","modifiedAt":1732226380,"id":"NEMzOTY4MTItRTMxMi00NUExLUE4NDYtRUFENEZDMTkyMDJC","creationAt":1732226380,"title":"Google","credentials":[{"urls":["google.com"],"type":"basic-auth","username":{"id":"MTdCOUI5NTUtM0FGOC00RDYzLUEwN0UtQjJFMjk1MTM1NDlC","fieldType":"string","value":"TestCXPGoogle"},"password":{"fieldType":"concealed-string","value":"1o23j1po3ij1o","id":"QTU2NDVDMTktMTgzQy00OEJELUI4NTMtNzg4NjYzRDk2NzI1"}}]}],"id":"RDQxRjU3QTYtM0NFNi00MTI5LUI0MkUtNUZBOUY0NkU3QTFD","collections":[],"email":"","userName":""}
|
||||
""" // swiftlint:disable:previous line_length
|
||||
}
|
||||
|
||||
func importCxf(payload: String) throws -> [BitwardenSdk.Cipher] {
|
||||
[.fixture(), .fixture()]
|
||||
}
|
||||
}
|
||||
|
||||
/// A temporary SDK Account to be used when exporting CXP.
|
||||
public struct BitwardenSdkAccount {
|
||||
let id: String
|
||||
let email: String
|
||||
let name: String?
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import AuthenticationServices
|
||||
import BitwardenSdk
|
||||
import Foundation
|
||||
|
||||
@ -42,6 +43,13 @@ protocol ExportVaultService: AnyObject {
|
||||
///
|
||||
func exportVaultFileContents(format: ExportFileType) async throws -> String
|
||||
|
||||
#if compiler(>=6.0.3)
|
||||
/// Exports the vault creating the `ASImportableAccount` to be used in Credential Exchange Protocol.
|
||||
/// - Returns: An `ASImportableAccount`
|
||||
@available(iOS 18.2, *)
|
||||
func exportVaultForCXP() async throws -> ASImportableAccount
|
||||
#endif
|
||||
|
||||
/// Generates a file name for the export file based on the current date, time, and specified extension.
|
||||
/// - Parameters:
|
||||
/// - prefix: An optional prefix to include in the file name. Defaults to nil.
|
||||
@ -188,6 +196,25 @@ class DefultExportVaultService: ExportVaultService {
|
||||
)
|
||||
}
|
||||
|
||||
#if compiler(>=6.0.3)
|
||||
|
||||
@available(iOS 18.2, *)
|
||||
func exportVaultForCXP() async throws -> ASImportableAccount {
|
||||
let ciphers = try await cipherService.fetchAllCiphers()
|
||||
.filter { $0.deletedDate == nil }
|
||||
|
||||
let account = try await stateService.getAccount(userId: nil)
|
||||
let sdkAccount = BitwardenSdkAccount(
|
||||
id: account.profile.userId,
|
||||
email: account.profile.email,
|
||||
name: account.profile.name
|
||||
)
|
||||
let serializedCXF = try await clientService.exporters().exportCxf(account: sdkAccount, ciphers: ciphers)
|
||||
return try JSONDecoder.cxpDecoder.decode(ASImportableAccount.self, from: Data(serializedCXF.utf8))
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
func generateExportFileName(
|
||||
prefix: String?,
|
||||
extension fileExtension: String
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import BitwardenSdk
|
||||
import InlineSnapshotTesting
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
|
||||
// MARK: - ExportVaultServiceTests
|
||||
|
||||
final class ExportVaultServiceTests: BitwardenTestCase {
|
||||
final class ExportVaultServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body_length
|
||||
// MARK: Properties
|
||||
|
||||
let cardCipher = Cipher(
|
||||
@ -192,6 +193,139 @@ final class ExportVaultServiceTests: BitwardenTestCase {
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
#if compiler(>=6.0.3)
|
||||
|
||||
/// `exportVaultForCXP()` CXP exporting the vault succeeds.
|
||||
func test_exportVaultForCXP_success() async throws {
|
||||
guard #available(iOS 18.2, *) else {
|
||||
throw XCTSkip("CXP Export is only available on iOS 18.2")
|
||||
}
|
||||
|
||||
stateService.activeAccount =
|
||||
.fixture(
|
||||
profile: .fixture(
|
||||
email: "example@example.com",
|
||||
name: "Test",
|
||||
userDecryptionOptions: nil,
|
||||
userId: "1"
|
||||
)
|
||||
)
|
||||
let ciphers: [Cipher] = [.fixture(), .fixture(deletedDate: .now)]
|
||||
cipherService.fetchAllCiphersResult = .success(ciphers)
|
||||
|
||||
let exportedResult = try XCTUnwrap(CXFFixtures.twoBasicAuthCiphers)
|
||||
clientService.mockExporters.exportCxfResult = .success(exportedResult)
|
||||
let result = try await subject.exportVaultForCXP()
|
||||
assertInlineSnapshot(of: result.dump(), as: .lines) {
|
||||
"""
|
||||
Email:
|
||||
UserName:
|
||||
--- Items ---
|
||||
Title: GitHub
|
||||
Type: login
|
||||
Creation: 2024-11-21 21:59:26 +0000
|
||||
Modified: 2024-11-21 21:59:26 +0000
|
||||
--- Credentials ---
|
||||
Username.FieldType: string
|
||||
Username.Value: TestCXP1
|
||||
Password.FieldType: concealedString
|
||||
Password.Value: adsfasf
|
||||
--- Urls ---
|
||||
github.com
|
||||
|
||||
Title: Google
|
||||
Type: login
|
||||
Creation: 2024-11-21 21:59:40 +0000
|
||||
Modified: 2024-11-21 21:59:40 +0000
|
||||
--- Credentials ---
|
||||
Username.FieldType: string
|
||||
Username.Value: TestCXPGoogle
|
||||
Password.FieldType: concealedString
|
||||
Password.Value: 1o23j1po3ij1o
|
||||
--- Urls ---
|
||||
google.com
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
/// `exportVaultForCXP()` CXP exporting throws when fetching ciphers.
|
||||
func test_exportVaultForCXP_throwsFetchingCiphers() async throws {
|
||||
guard #available(iOS 18.2, *) else {
|
||||
throw XCTSkip("CXP Export is only available on iOS 18.2")
|
||||
}
|
||||
|
||||
cipherService.fetchAllCiphersResult = .failure(BitwardenTestError.example)
|
||||
await assertAsyncThrows(error: BitwardenTestError.example) {
|
||||
_ = try await subject.exportVaultForCXP()
|
||||
}
|
||||
}
|
||||
|
||||
/// `exportVaultForCXP()` CXP exporting throws when getting account.
|
||||
func test_exportVaultForCXP_throwsGettingAccount() async throws {
|
||||
guard #available(iOS 18.2, *) else {
|
||||
throw XCTSkip("CXP Export is only available on iOS 18.2")
|
||||
}
|
||||
|
||||
stateService.activeAccount = nil
|
||||
let ciphers: [Cipher] = [.fixture(), .fixture(deletedDate: .now)]
|
||||
cipherService.fetchAllCiphersResult = .success(ciphers)
|
||||
await assertAsyncThrows(error: StateServiceError.noActiveAccount) {
|
||||
_ = try await subject.exportVaultForCXP()
|
||||
}
|
||||
}
|
||||
|
||||
/// `exportVaultForCXP()` CXP exporting throws when exporting.
|
||||
func test_exportVaultForCXP_throwsExporting() async throws {
|
||||
guard #available(iOS 18.2, *) else {
|
||||
throw XCTSkip("CXP Export is only available on iOS 18.2")
|
||||
}
|
||||
|
||||
stateService.activeAccount =
|
||||
.fixture(
|
||||
profile: .fixture(
|
||||
email: "example@example.com",
|
||||
name: "Test",
|
||||
userDecryptionOptions: nil,
|
||||
userId: "1"
|
||||
)
|
||||
)
|
||||
let ciphers: [Cipher] = [.fixture(), .fixture(deletedDate: .now)]
|
||||
cipherService.fetchAllCiphersResult = .success(ciphers)
|
||||
clientService.mockExporters.exportCxfResult = .failure(BitwardenTestError.example)
|
||||
await assertAsyncThrows(error: BitwardenTestError.example) {
|
||||
_ = try await subject.exportVaultForCXP()
|
||||
}
|
||||
}
|
||||
|
||||
/// `exportVaultForCXP()` CXP exporting throws when decoding exported data.
|
||||
func test_exportVaultForCXP_throwsWhenDecoding() async throws {
|
||||
guard #available(iOS 18.2, *) else {
|
||||
throw XCTSkip("CXP Export is only available on iOS 18.2")
|
||||
}
|
||||
|
||||
stateService.activeAccount =
|
||||
.fixture(
|
||||
profile: .fixture(
|
||||
email: "example@example.com",
|
||||
name: "Test",
|
||||
userDecryptionOptions: nil,
|
||||
userId: "1"
|
||||
)
|
||||
)
|
||||
let ciphers: [Cipher] = [.fixture(), .fixture(deletedDate: .now)]
|
||||
cipherService.fetchAllCiphersResult = .success(ciphers)
|
||||
clientService.mockExporters.exportCxfResult = .success("{somethingthatcantbedecoded}")
|
||||
do {
|
||||
_ = try await subject.exportVaultForCXP()
|
||||
} catch DecodingError.dataCorrupted {
|
||||
XCTAssert(true)
|
||||
} catch {
|
||||
XCTFail("Export vault threw unexpected error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// Test the exporter receives the correct content for CSV export type.
|
||||
///
|
||||
func test_fileContent_csv() async throws {
|
||||
@ -297,4 +431,4 @@ final class ExportVaultServiceTests: BitwardenTestCase {
|
||||
let name = subject.generateExportFileName(extension: fileType.fileExtension)
|
||||
XCTAssertEqual(name, expectedName)
|
||||
}
|
||||
}
|
||||
} // swiftlint:disable:this file_length
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
// swiftlint:disable:this file_name
|
||||
|
||||
/// Fixtures for Credential Exchange flows.
|
||||
enum CXFFixtures {
|
||||
/// Fixture to be used on export flow with two basic-auth ciphers.
|
||||
static let twoBasicAuthCiphers = TestDataHelpers.loadUTFStringFromJsonBundle(resource: "cxfTwoBasicAuthCiphers")
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"modifiedAt": 1732226366,
|
||||
"creationAt": 1732226366,
|
||||
"title": "GitHub",
|
||||
"credentials": [
|
||||
{
|
||||
"urls": [
|
||||
"github.com"
|
||||
],
|
||||
"password": {
|
||||
"id": "RTEzRDEwQjctRTdCQy00QTI3LTgwNDAtRjgxMzNBOTMxMjhC",
|
||||
"fieldType": "concealed-string",
|
||||
"value": "adsfasf"
|
||||
},
|
||||
"type": "basic-auth",
|
||||
"username": {
|
||||
"fieldType": "string",
|
||||
"id": "NTlBMUFBNUYtODE5My00QUIzLThGRjYtOEFCRUQ5MUQxNUZG",
|
||||
"value": "TestCXP1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": "MjZDQzQwQTQtQUZDQS00NEIzLUEwNjAtMUMyNUUzNTc1RTZB",
|
||||
"type": "login"
|
||||
},
|
||||
{
|
||||
"type": "login",
|
||||
"modifiedAt": 1732226380,
|
||||
"id": "NEMzOTY4MTItRTMxMi00NUExLUE4NDYtRUFENEZDMTkyMDJC",
|
||||
"creationAt": 1732226380,
|
||||
"title": "Google",
|
||||
"credentials": [
|
||||
{
|
||||
"urls": [
|
||||
"google.com"
|
||||
],
|
||||
"type": "basic-auth",
|
||||
"username": {
|
||||
"id": "MTdCOUI5NTUtM0FGOC00RDYzLUEwN0UtQjJFMjk1MTM1NDlC",
|
||||
"fieldType": "string",
|
||||
"value": "TestCXPGoogle"
|
||||
},
|
||||
"password": {
|
||||
"fieldType": "concealed-string",
|
||||
"value": "1o23j1po3ij1o",
|
||||
"id": "QTU2NDVDMTktMTgzQy00OEJELUI4NTMtNzg4NjYzRDk2NzI1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"id": "RDQxRjU3QTYtM0NFNi00MTI5LUI0MkUtNUZBOUY0NkU3QTFD",
|
||||
"collections": [],
|
||||
"email": "",
|
||||
"userName": ""
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
#if compiler(>=6.0.3)
|
||||
|
||||
import AuthenticationServices
|
||||
|
||||
@available(iOS 18.2, *)
|
||||
extension ASImportableAccount {
|
||||
/// Dumps the content of the `ASImportableAccount` into lines which can be used with
|
||||
/// inline snapshot assertion.
|
||||
func dump() -> String { // swiftlint:disable:this cyclomatic_complexity function_body_length
|
||||
var dumpResult = ""
|
||||
dumpResult.append("Email: \(email)\n")
|
||||
dumpResult.append("UserName: \(userName)\n")
|
||||
dumpResult.append("--- Items ---\n")
|
||||
|
||||
let itemsResult = items.reduce(into: "") { result, item in
|
||||
result.appendWithIndentation("Title: \(item.title)\n")
|
||||
result.appendWithIndentation("Type: \(item.type)\n")
|
||||
result.appendWithIndentation("Creation: \(item.created)\n")
|
||||
result.appendWithIndentation("Modified: \(item.lastModified)\n")
|
||||
result.appendWithIndentation("--- Credentials ---\n")
|
||||
|
||||
let credentialsResult = item.credentials.reduce(into: "") { credResult, credential in
|
||||
switch credential {
|
||||
case let .basicAuthentication(basicAuthentication):
|
||||
if let username = basicAuthentication.username {
|
||||
credResult.appendWithIndentation("Username.FieldType: \(username.fieldType)\n", level: 2)
|
||||
credResult.appendWithIndentation("Username.Value: \(username.value)\n", level: 2)
|
||||
}
|
||||
if let password = basicAuthentication.password {
|
||||
credResult.appendWithIndentation("Password.FieldType: \(password.fieldType)\n", level: 2)
|
||||
credResult.appendWithIndentation("Password.Value: \(password.value)\n", level: 2)
|
||||
}
|
||||
if !basicAuthentication.urls.isEmpty {
|
||||
credResult.appendWithIndentation("--- Urls ---\n", level: 2)
|
||||
let urlsResult = basicAuthentication.urls.reduce(into: "") { urlResult, url in
|
||||
urlResult.appendWithIndentation(url, level: 3)
|
||||
if url != basicAuthentication.urls.last {
|
||||
urlResult.appendWithIndentation("\n\n", level: 3)
|
||||
}
|
||||
}
|
||||
credResult.appendWithIndentation(urlsResult, level: 2)
|
||||
}
|
||||
case let .passkey(passkey):
|
||||
credResult.appendWithIndentation("CredentialID: \(passkey.credentialID)\n", level: 2)
|
||||
credResult.appendWithIndentation("Key: \(passkey.key)\n", level: 2)
|
||||
credResult.appendWithIndentation(
|
||||
"RelyingPartyIdentifier: \(passkey.relyingPartyIdentifier)\n",
|
||||
level: 2
|
||||
)
|
||||
credResult.appendWithIndentation("UserDisplayName: \(passkey.userDisplayName)\n", level: 2)
|
||||
credResult.appendWithIndentation("Username: \(passkey.userName)\n", level: 2)
|
||||
case let .totp(totp):
|
||||
credResult.appendWithIndentation("Algorithm: \(totp.algorithm)\n", level: 2)
|
||||
credResult.appendWithIndentation("Digits: \(totp.digits)\n", level: 2)
|
||||
if let issuer = totp.issuer {
|
||||
credResult.appendWithIndentation("Issuer: \(issuer)\n", level: 2)
|
||||
}
|
||||
credResult.appendWithIndentation("Period: \(totp.period)\n", level: 2)
|
||||
credResult.appendWithIndentation("Secret: \(totp.secret)\n", level: 2)
|
||||
credResult.appendWithIndentation("Username: \(totp.username)\n", level: 2)
|
||||
case let .note(note):
|
||||
credResult.appendWithIndentation("Note: \(note.content)\n", level: 2)
|
||||
case let .creditCard(card):
|
||||
credResult.appendWithIndentation("FullName: \(card.fullName)\n", level: 2)
|
||||
credResult.appendWithIndentation("Number: \(card.number)\n", level: 2)
|
||||
if let cardType = card.cardType {
|
||||
credResult.appendWithIndentation("CardType: \(cardType)\n", level: 2)
|
||||
}
|
||||
if let expiryDate = card.expiryDate {
|
||||
credResult.appendWithIndentation("ExpiryDate: \(expiryDate)\n", level: 2)
|
||||
}
|
||||
if let validFrom = card.validFrom {
|
||||
credResult.appendWithIndentation("ValidFrom: \(validFrom)\n", level: 2)
|
||||
}
|
||||
if let verificationNumber = card.verificationNumber {
|
||||
credResult.appendWithIndentation("VerificationNumber: \(verificationNumber)\n", level: 2)
|
||||
}
|
||||
@unknown default:
|
||||
result.append("unknown default\n")
|
||||
}
|
||||
if credential != item.credentials.last {
|
||||
credResult.appendWithIndentation("\n\n", level: 2)
|
||||
}
|
||||
}
|
||||
result.append(credentialsResult)
|
||||
if item != items.last {
|
||||
result.append("\n\n")
|
||||
}
|
||||
}
|
||||
dumpResult.append(itemsResult)
|
||||
return dumpResult
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
mutating func appendWithIndentation(_ other: String, level: Int = 1) {
|
||||
let indentation = String(repeating: " ", count: level * 2)
|
||||
append("\(indentation)\(other)")
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@ -86,7 +86,12 @@ extension CipherListViewType {
|
||||
case .identity:
|
||||
self = .identity
|
||||
case .login:
|
||||
self = .login(hasFido2: !(cipher.login?.fido2Credentials?.isEmpty ?? true), totp: cipher.login?.totp)
|
||||
self = .login(
|
||||
hasFido2: !(
|
||||
cipher.login?.fido2Credentials?.isEmpty ?? true
|
||||
),
|
||||
totp: cipher.login?.totp
|
||||
)
|
||||
case .secureNote:
|
||||
self = .secureNote
|
||||
case .sshKey:
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import BitwardenSdk
|
||||
|
||||
@testable import BitwardenShared
|
||||
|
||||
// MARK: - MockClientExporters
|
||||
|
||||
/// A mocked `ClientExportersProtocol`.
|
||||
@ -7,12 +9,19 @@ import BitwardenSdk
|
||||
class MockClientExporters {
|
||||
// MARK: Properties
|
||||
|
||||
/// The ciphers exported in a call to `exportVault(_:)` or `exportOrganizationVault(_:)`.
|
||||
/// The account used in `exportOrganizationVault(_:)`.
|
||||
var account: BitwardenSdkAccount?
|
||||
|
||||
/// The ciphers exported in a call to `exportVault(_:)` or `exportOrganizationVault(_:)`
|
||||
/// or `exportOrganizationVault(_:)`.
|
||||
var ciphers = [BitwardenSdk.Cipher]()
|
||||
|
||||
/// The collections exported in a call to `exportOrganizationVault(_:)`.
|
||||
var collections = [BitwardenSdk.Collection]()
|
||||
|
||||
/// The result of a call to `exportCxf(account:ciphers:)`
|
||||
var exportCxfResult: Result<String, Error> = .failure(BitwardenTestError.example)
|
||||
|
||||
/// The result of a call to `exportOrganizationVault(_:)`
|
||||
var exportOrganizationVaultResult: Result<String, Error> = .failure(BitwardenTestError.example)
|
||||
|
||||
@ -24,11 +33,23 @@ class MockClientExporters {
|
||||
|
||||
/// The format of the export in a call to `exportVault(_:)` or `exportOrganizationVault(_:)`.
|
||||
var format: BitwardenSdk.ExportFormat?
|
||||
|
||||
/// The payload passed to `importCxf(payload:)`
|
||||
var importCxfPayload: String?
|
||||
|
||||
/// The result of a call to `importCxf(payload:)`
|
||||
var importCxfResult: Result<[BitwardenSdk.Cipher], Error> = .failure(BitwardenTestError.example)
|
||||
}
|
||||
|
||||
// MARK: - ClientExportersProtocol
|
||||
|
||||
extension MockClientExporters: ClientExportersProtocol {
|
||||
extension MockClientExporters: ClientExportersServiceTemp {
|
||||
func exportCxf(account: BitwardenSdkAccount, ciphers: [BitwardenSdk.Cipher]) throws -> String {
|
||||
self.account = account
|
||||
self.ciphers = ciphers
|
||||
return try exportCxfResult.get()
|
||||
}
|
||||
|
||||
func exportOrganizationVault(
|
||||
collections: [BitwardenSdk.Collection],
|
||||
ciphers: [BitwardenSdk.Cipher],
|
||||
@ -50,4 +71,9 @@ extension MockClientExporters: ClientExportersProtocol {
|
||||
self.format = format
|
||||
return try exportVaultResult.get()
|
||||
}
|
||||
|
||||
func importCxf(payload: String) throws -> [BitwardenSdk.Cipher] {
|
||||
importCxfPayload = payload
|
||||
return try importCxfResult.get()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import AuthenticationServices
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
|
||||
class MockExportVaultService: ExportVaultService {
|
||||
var didClearFiles = false
|
||||
|
||||
var exportVaultForCXPResult: Result<ImportableAccountProxy, Error> = .failure(BitwardenTestError.example)
|
||||
var exportVaultContentsFormat: ExportFileType?
|
||||
var exportVaultContentResult: Result<String, Error> = .failure(BitwardenTestError.example)
|
||||
|
||||
@ -16,6 +19,16 @@ class MockExportVaultService: ExportVaultService {
|
||||
didClearFiles = true
|
||||
}
|
||||
|
||||
#if compiler(>=6.0.3)
|
||||
@available(iOS 18.2, *)
|
||||
func exportVaultForCXP() async throws -> ASImportableAccount {
|
||||
guard let result = try exportVaultForCXPResult.get() as? ASImportableAccount else {
|
||||
throw MockExportVaultServiceError.unableToCastToASImportableAccount
|
||||
}
|
||||
return result
|
||||
}
|
||||
#endif
|
||||
|
||||
func exportVaultFileContents(format: BitwardenShared.ExportFileType) async throws -> String {
|
||||
exportVaultContentsFormat = format
|
||||
return try exportVaultContentResult.get()
|
||||
@ -32,3 +45,14 @@ class MockExportVaultService: ExportVaultService {
|
||||
try writeToFileResult.get()
|
||||
}
|
||||
}
|
||||
|
||||
protocol ImportableAccountProxy {}
|
||||
|
||||
#if compiler(>=6.0.3)
|
||||
@available(iOS 18.2, *)
|
||||
extension ASImportableAccount: ImportableAccountProxy {}
|
||||
#endif
|
||||
|
||||
enum MockExportVaultServiceError: Error {
|
||||
case unableToCastToASImportableAccount
|
||||
}
|
||||
|
||||
@ -216,6 +216,15 @@ public class AppProcessor {
|
||||
)))
|
||||
}
|
||||
|
||||
/// Handles importing credentials using Credential Exchange Protocol.
|
||||
/// - Parameter credentialImportToken: The credentials import token to user with the `ASCredentialImportManager`.
|
||||
@available(iOSApplicationExtension 18.2, *)
|
||||
public func handleImportCredentials(credentialImportToken: UUID) {
|
||||
// TODO: PM-14800 Move this to a specific view to handle importing process
|
||||
// and handle credential data.
|
||||
// let credentialData = try await ASCredentialImportManager().importCredentials(token: credentialImportToken)
|
||||
}
|
||||
|
||||
// MARK: Autofill Methods
|
||||
|
||||
/// Returns a `ASPasswordCredential` that matches the user-requested credential which can be
|
||||
|
||||
@ -1059,3 +1059,5 @@
|
||||
"CopyPrivateKey" = "Copy private key";
|
||||
"CopyFingerprint" = "Copy fingerprint";
|
||||
"SSHKeys" = "SSH keys";
|
||||
"ExportingFailed" = "Exporting failed";
|
||||
"YouMayNeedToEnableDevicePasscodeOrBiometrics" = "You may need to enable device passcode or biometrics.";
|
||||
|
||||
25
Scripts/alpha_update_cxp_infoplist.sh
Executable file
25
Scripts/alpha_update_cxp_infoplist.sh
Executable file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Updates the Info.plist from Bitwarden Autofill extension to support Credential Exchange.
|
||||
#
|
||||
# :warning: Note: This script should be removed when we're ready to relase CXP to production.
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# $ ./alpha_update_cxp_infoplist.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
autofill_info_plist_path="BitwardenAutoFillExtension/Application/Support/Info.plist"
|
||||
app_info_plist_path="Bitwarden/Application/Support/Info.plist"
|
||||
|
||||
if ! grep -q "SupportsCredentialExchange" $autofill_info_plist_path; then
|
||||
plutil -insert NSExtension.NSExtensionAttributes.ASCredentialProviderExtensionCapabilities.SupportsCredentialExchange -bool YES $autofill_info_plist_path
|
||||
fi
|
||||
|
||||
if ! grep -q "ASCredentialExchangeActivityType" $app_info_plist_path; then
|
||||
if ! grep -q "NSUserActivityTypes" $app_info_plist_path; then
|
||||
plutil -insert NSUserActivityTypes -array $app_info_plist_path
|
||||
fi
|
||||
plutil -insert NSUserActivityTypes -string ASCredentialExchangeActivityType -append $app_info_plist_path
|
||||
fi
|
||||
@ -88,3 +88,7 @@ cat << EOF > ${export_options_file}
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
if [[ $compiler_flags == *"SUPPORTS_CXP"* ]]; then
|
||||
./alpha_update_cxp_infoplist.sh
|
||||
fi
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user