Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-14800] Implement import flow in Credential Exchange protocol #1159

Draft
wants to merge 16 commits into
base: PM-14800/credential-exchange
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
43 changes: 37 additions & 6 deletions Bitwarden/Application/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
let incomingURL = userActivity.webpageURL {
appProcessor.handleAppLinks(incomingURL: incomingURL)
}

#if compiler(>=6.0.3)

if #available(iOS 18.2, *),
let userActivity = connectionOptions.userActivities.first {
await checkAndHandleCredentialExchangeActivity(appProcessor: appProcessor, userActivity: userActivity)
}

#endif
}
}

Expand All @@ -90,13 +99,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {

#if compiler(>=6.0.3)

if #available(iOS 18.2, *),
userActivity.activityType == ASCredentialExchangeActivity {
guard let token = userActivity.userInfo?[ASCredentialImportToken] as? UUID else {
return
if #available(iOS 18.2, *) {
Task {
await checkAndHandleCredentialExchangeActivity(appProcessor: appProcessor, userActivity: userActivity)
}

appProcessor.handleImportCredentials(credentialImportToken: token)
}

#endif
Expand Down Expand Up @@ -173,3 +179,28 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
#endif
}

// MARK: - SceneDelegate 18.2

#if compiler(>=6.0.3)

@available(iOS 18.2, *)
extension SceneDelegate {
/// Checks whether there is an `ASCredentialExchangeActivity` in the `userActivity` and handles it.
/// - Parameters:
/// - appProcessor: The `AppProcessor` to handle the logic.
/// - userActivity: The activity to handle.
private func checkAndHandleCredentialExchangeActivity(
appProcessor: AppProcessor,
userActivity: NSUserActivity
) async {
guard userActivity.activityType == ASCredentialExchangeActivity,
let token = userActivity.userInfo?[ASCredentialImportToken] as? UUID else {
return
}

await appProcessor.handleImportCredentials(credentialImportToken: token)
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/// `AnyKey` is a `CodingKey` type that can be used for encoding and decoding keys for custom
/// key decoding strategies.
struct AnyKey: CodingKey {
let stringValue: String
let intValue: Int?

init(stringValue: String) {
self.stringValue = stringValue
intValue = nil
}

init(intValue: Int) {
stringValue = String(intValue)
self.intValue = intValue
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,6 @@
import Foundation

extension JSONDecoder {
// MARK: Types

/// `AnyKey` is a `CodingKey` type that can be used for encoding and decoding keys for custom
/// key decoding strategies.
struct AnyKey: CodingKey {
let stringValue: String
let intValue: Int?

init(stringValue: String) {
self.stringValue = stringValue
intValue = nil
}

init(intValue: Int) {
stringValue = String(intValue)
self.intValue = intValue
}
}

// MARK: Static Properties

/// The default `JSONDecoder` used to decode JSON payloads throughout the app.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,32 @@ extension JSONEncoder {
}
return jsonEncoder
}()

/// The default `JSONEncoder` used to encode JSON payloads when in Credential Exchange flow.
static let cxpEncoder: JSONEncoder = {
let jsonEncoder = JSONEncoder()
jsonEncoder.dateEncodingStrategy = .custom { date, encoder in
var container = encoder.singleValueContainer()
try container.encode(Int(date.timeIntervalSince1970))
}
jsonEncoder.keyEncodingStrategy = .custom { keys in
let key = keys.last!.stringValue
return AnyKey(stringValue: customTransformCodingKeyForCXP(key: key))
}
return jsonEncoder
}()

// MARK: Static Functions

/// Transforms the keys from CXP format handled by the Bitwarden SDK into the keys that Apple expects.
static func customTransformCodingKeyForCXP(key: String) -> String {
return switch key {
case "credentialID":
"credentialId"
case "rpID":
"rpId"
default:
key
}
}
}
16 changes: 16 additions & 0 deletions BitwardenShared/Core/Platform/Services/ServiceContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
/// The repository used by the application to manage generator data for the UI layer.
let generatorRepository: GeneratorRepository

/// The repository used by the application to manage importing credential in Credential Exhange flow.
let importCiphersRepository: ImportCiphersRepository

/// The service used to access & store data on the device keychain.
let keychainService: KeychainService

Expand Down Expand Up @@ -183,6 +186,8 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
/// and extends the capabilities of the `Fido2UserInterface` from the SDK.
/// - fido2CredentialStore: A store to be used on Fido2 flows to get/save credentials.
/// - generatorRepository: The repository used by the application to manage generator data for the UI layer.
/// - importCiphersRepository: The repository used by the application to manage importing credential
/// in Credential Exhange flow.
/// - keychainRepository: The repository used to manages keychain items.
/// - keychainService: The service used to access & store data on the device keychain.
/// - localAuthService: The service used by the application to evaluate local auth policies.
Expand Down Expand Up @@ -230,6 +235,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
fido2CredentialStore: Fido2CredentialStore,
fido2UserInterfaceHelper: Fido2UserInterfaceHelper,
generatorRepository: GeneratorRepository,
importCiphersRepository: ImportCiphersRepository,
keychainRepository: KeychainRepository,
keychainService: KeychainService,
localAuthService: LocalAuthService,
Expand Down Expand Up @@ -276,6 +282,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
self.fido2CredentialStore = fido2CredentialStore
self.fido2UserInterfaceHelper = fido2UserInterfaceHelper
self.generatorRepository = generatorRepository
self.importCiphersRepository = importCiphersRepository
self.keychainService = keychainService
self.keychainRepository = keychainRepository
self.localAuthService = localAuthService
Expand Down Expand Up @@ -635,6 +642,14 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
vaultTimeoutService: vaultTimeoutService
)

let importCiphersRepository = DefaultImportCiphersRepository(
clientService: clientService,
importCiphersService: DefaultImportCiphersService(
importCiphersAPIService: apiService
),
syncService: syncService
)

let authenticatorDataStore = AuthenticatorBridgeDataStore(
errorReporter: errorReporter,
groupIdentifier: Bundle.main.sharedAppGroupIdentifier,
Expand Down Expand Up @@ -692,6 +707,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
fido2CredentialStore: fido2CredentialStore,
fido2UserInterfaceHelper: fido2UserInterfaceHelper,
generatorRepository: generatorRepository,
importCiphersRepository: importCiphersRepository,
keychainRepository: keychainRepository,
keychainService: keychainService,
localAuthService: localAuthService,
Expand Down
8 changes: 8 additions & 0 deletions BitwardenShared/Core/Platform/Services/Services.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ typealias Services = HasAPIService
& HasFido2UserInterfaceHelper
& HasFileAPIService
& HasGeneratorRepository
& HasImportCiphersRepository
& HasLocalAuthService
& HasNFCReaderService
& HasNotificationCenterService
Expand Down Expand Up @@ -207,6 +208,13 @@ protocol HasGeneratorRepository {
var generatorRepository: GeneratorRepository { get }
}

/// Protocol for an object that provides a `ImportCiphersRepository`.
///
protocol HasImportCiphersRepository {
/// The repository used by the application to manage importing credential in Credential Exhange flow.
var importCiphersRepository: ImportCiphersRepository { get }
}

/// Protocol for an object that provides a `LocalAuthService`.
///
protocol HasLocalAuthService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Networking

@testable import BitwardenShared

extension ServiceContainer {
extension ServiceContainer { // swiftlint:disable:this function_body_length
static func withMocks(
application: Application? = nil,
appSettingsStore: AppSettingsStore = MockAppSettingsStore(),
Expand All @@ -24,6 +24,7 @@ extension ServiceContainer {
fido2CredentialStore: Fido2CredentialStore = MockFido2CredentialStore(),
fido2UserInterfaceHelper: Fido2UserInterfaceHelper = MockFido2UserInterfaceHelper(),
generatorRepository: GeneratorRepository = MockGeneratorRepository(),
importCiphersRepository: ImportCiphersRepository = MockImportCiphersRepository(),
httpClient: HTTPClient = MockHTTPClient(),
keychainRepository: KeychainRepository = MockKeychainRepository(),
keychainService: KeychainService = MockKeychainService(),
Expand Down Expand Up @@ -75,6 +76,7 @@ extension ServiceContainer {
fido2CredentialStore: fido2CredentialStore,
fido2UserInterfaceHelper: fido2UserInterfaceHelper,
generatorRepository: generatorRepository,
importCiphersRepository: importCiphersRepository,
keychainRepository: keychainRepository,
keychainService: keychainService,
localAuthService: localAuthService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,12 @@ struct ImportCiphersRequestModel: JSONRequestBody {

/// The cipher<->folder relationships map. The key is the cipher index and the value is the folder index
/// in their respective arrays.
var folderRelationships: [Int: Int]
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 {
let key: Int
let value: Int
}
Loading
Loading