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-15634] Add intermediate Export settings view #1227

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
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
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
@@ -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 All @@ -39,9 +20,9 @@
// not both at the same time ๐Ÿ™ƒ. Since the API contains both formats with the more
// common containing fractional seconds, attempt to parse that first and fall back to
// parsing without fractional seconds.
if let date = dateFormatterWithFractionalSeconds.date(from: stringValue) {

Check warning on line 23 in BitwardenShared/Core/Platform/Services/API/Extensions/JSONDecoder+Bitwarden.swift

View workflow job for this annotation

GitHub Actions / Test

capture of 'dateFormatterWithFractionalSeconds' with non-sendable type 'ISO8601DateFormatter' in a `@Sendable` closure; this is an error in the Swift 6 language mode
return date
} else if let date = dateFormatter.date(from: stringValue) {

Check warning on line 25 in BitwardenShared/Core/Platform/Services/API/Extensions/JSONDecoder+Bitwarden.swift

View workflow job for this annotation

GitHub Actions / Test

capture of 'dateFormatter' with non-sendable type 'ISO8601DateFormatter' in a `@Sendable` closure; this is an error in the Swift 6 language mode
return date
} else {
throw DecodingError.dataCorruptedError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,32 @@ import XCTest
class JSONDecoderBitwardenTests: BitwardenTestCase {
// MARK: Tests

/// `JSONDecoder.cxpDecoder` can decode Credential Exchange Format.
func test_cxpDecoder_decodesISO8601DateWithFractionalSeconds() {
let subject = JSONDecoder.cxpDecoder
let toDecode = #"{"credentialId":"credential","date":1697790414,"otherKey":"other","rpId":"rp"}"#

struct JSONBody: Codable, Equatable {
let credentialID: String
let date: Date
let otherKey: String
let rpID: String
}

let body = JSONBody(
credentialID: "credential",
date: Date(year: 2023, month: 10, day: 20, hour: 8, minute: 26, second: 54),
otherKey: "other",
rpID: "rp"
)

XCTAssertEqual(
try subject
.decode(JSONBody.self, from: Data(toDecode.utf8)),
body
)
}

/// `JSONDecoder.defaultDecoder` can decode ISO8601 dates with fractional seconds.
func test_defaultDecoder_decodesISO8601DateWithFractionalSeconds() {
let subject = JSONDecoder.defaultDecoder
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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,32 @@ import XCTest
class JSONEncoderBitwardenTests: BitwardenTestCase {
// MARK: Tests

/// `JSONEncoder.cxpEncoder` encodes for Credential Exchange Format.
func test_cxfpEncoder_encodesISO8601DateWithFractionalSeconds() throws {
let subject = JSONEncoder.cxpEncoder
subject.outputFormatting = .sortedKeys // added for test consistency so output is ordered.

struct JSONBody: Codable {
let credentialID: String
let date: Date
let otherKey: String
let rpID: String
}

let body = JSONBody(
credentialID: "credential",
date: Date(year: 2023, month: 10, day: 20, hour: 8, minute: 26, second: 54),
otherKey: "other",
rpID: "rp"
)
let encodedData = try subject.encode(body)
let encodedString = String(data: encodedData, encoding: .utf8)
XCTAssertEqual(
encodedString,
#"{"credentialId":"credential","date":1697790414,"otherKey":"other","rpId":"rp"}"#
)
}

/// `JSONEncoder.defaultEncoder` can encode ISO8601 dates without fractional seconds.
func test_defaultEncoder_encodesISO8601DateWithoutFractionalSeconds() throws {
let subject = JSONEncoder.defaultEncoder
Expand Down
10 changes: 5 additions & 5 deletions BitwardenShared/Core/Platform/Services/ClientService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 -> ClientExportersServiceTemp
func exporters(for userId: String?) async throws -> ClientExportersProtocol

/// Returns a `ClientGeneratorsProtocol` for generator data tasks.
///
Expand Down Expand Up @@ -88,7 +88,7 @@ extension ClientService {

/// Returns a `ClientExportersProtocol` for vault export data tasks.
///
func exporters() async throws -> ClientExportersServiceTemp {
func exporters() async throws -> ClientExportersProtocol {
try await exporters(for: nil)
}

Expand Down Expand Up @@ -199,7 +199,7 @@ actor DefaultClientService: ClientService {
try await client(for: userId).crypto()
}

func exporters(for userId: String?) async throws -> ClientExportersServiceTemp {
func exporters(for userId: String?) async throws -> ClientExportersProtocol {
try await client(for: userId).exporters()
}

Expand Down Expand Up @@ -366,7 +366,7 @@ protocol BitwardenSdkClient {
func crypto() -> ClientCryptoProtocol

/// Returns exporters.
func exporters() -> ClientExportersServiceTemp
func exporters() -> ClientExportersProtocol

/// Returns generator operations.
func generators() -> ClientGeneratorsProtocol
Expand All @@ -392,7 +392,7 @@ extension Client: BitwardenSdkClient {
crypto() as ClientCrypto
}

func exporters() -> ClientExportersServiceTemp {
func exporters() -> ClientExportersProtocol {
exporters() as ClientExporters
}

Expand Down
19 changes: 19 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 @@ -192,6 +195,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 @@ -241,6 +246,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 @@ -290,6 +296,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 @@ -657,6 +664,17 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
vaultTimeoutService: vaultTimeoutService
)

let credentialManagerFactory = DefaultCredentialManagerFactory()

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

let userVerificationHelperFactory = DefaultUserVerificationHelperFactory(
authRepository: authRepository,
errorReporter: errorReporter,
Expand Down Expand Up @@ -728,6 +746,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 @@ -210,6 +211,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 @@ -37,7 +37,7 @@ class MockClient: BitwardenSdkClient {
""
}

func exporters() -> any ClientExportersServiceTemp {
func exporters() -> any ClientExportersProtocol {
clientExporters
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class MockClientService: ClientService {
mockCrypto
}

func exporters(for userId: String?) -> ClientExportersServiceTemp {
func exporters(for userId: String?) -> ClientExportersProtocol {
mockExporters
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 @@ -78,6 +79,7 @@ extension ServiceContainer {
fido2CredentialStore: fido2CredentialStore,
fido2UserInterfaceHelper: fido2UserInterfaceHelper,
generatorRepository: generatorRepository,
importCiphersRepository: importCiphersRepository,
keychainRepository: keychainRepository,
keychainService: keychainService,
localAuthService: localAuthService,
Expand Down
16 changes: 16 additions & 0 deletions BitwardenShared/Core/Platform/Utilities/AnyKey.swift
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
}
}
Loading
Loading