From 6af83fce35e30403e7b9c468661271724e709437 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:51:57 -0400 Subject: [PATCH] [BITAU-137] Create AuthenticatorSyncKit SDK (#913) --- .../AuthenticatorKeychainService.swift | 38 ++++ AuthenticatorSyncKit/Info.plist | 18 ++ .../SharedKeychainRepository.swift | 155 ++++++++++++++ .../Tests/SharedKeychainRepositoryTests.swift | 126 +++++++++++ .../MockAuthenticatorKeychainService.swift | 40 ++++ .../MockSharedKeychainRepository.swift | 31 +++ .../AuthenticatorSyncKitTestCase.swift | 198 ++++++++++++++++++ .../Tests/TestHelpers/Support/Info.plist | 22 ++ .../Core/Auth/Services/KeychainService.swift | 5 + Configs/AuthenticatorSyncKit.xcconfig | 4 + Package.swift | 32 +++ project.yml | 41 ++++ 12 files changed, 710 insertions(+) create mode 100644 AuthenticatorSyncKit/AuthenticatorKeychainService.swift create mode 100644 AuthenticatorSyncKit/Info.plist create mode 100644 AuthenticatorSyncKit/SharedKeychainRepository.swift create mode 100644 AuthenticatorSyncKit/Tests/SharedKeychainRepositoryTests.swift create mode 100644 AuthenticatorSyncKit/Tests/TestHelpers/MockAuthenticatorKeychainService.swift create mode 100644 AuthenticatorSyncKit/Tests/TestHelpers/MockSharedKeychainRepository.swift create mode 100644 AuthenticatorSyncKit/Tests/TestHelpers/Support/AuthenticatorSyncKitTestCase.swift create mode 100644 AuthenticatorSyncKit/Tests/TestHelpers/Support/Info.plist create mode 100644 Configs/AuthenticatorSyncKit.xcconfig create mode 100644 Package.swift diff --git a/AuthenticatorSyncKit/AuthenticatorKeychainService.swift b/AuthenticatorSyncKit/AuthenticatorKeychainService.swift new file mode 100644 index 000000000..2a55a0129 --- /dev/null +++ b/AuthenticatorSyncKit/AuthenticatorKeychainService.swift @@ -0,0 +1,38 @@ +import Foundation + +// MARK: - AuthenticatorKeychainService + +/// A Service to provide a wrapper around the device keychain shared via App Group between +/// the Authenticator and the main Bitwarden app. +/// +public protocol AuthenticatorKeychainService: AnyObject { + /// Adds a set of attributes. + /// + /// - Parameter attributes: Attributes to add. + /// + func add(attributes: CFDictionary) throws + + /// Attempts a deletion based on a query. + /// + /// - Parameter query: Query for the delete. + /// + func delete(query: CFDictionary) throws + + /// Searches for a query. + /// + /// - Parameter query: Query for the search. + /// - Returns: The search results. + /// + func search(query: CFDictionary) throws -> AnyObject? +} + +// MARK: - AuthenticatorKeychainServiceError + +/// Enum with possible error cases that can be thrown from `AuthenticatorKeychainService`. +public enum AuthenticatorKeychainServiceError: Error, Equatable { + /// When a `KeychainService` is unable to locate an auth key for a given storage key. + /// + /// - Parameter KeychainItem: The potential storage key for the auth key. + /// + case keyNotFound(SharedKeychainItem) +} diff --git a/AuthenticatorSyncKit/Info.plist b/AuthenticatorSyncKit/Info.plist new file mode 100644 index 000000000..621b0bd75 --- /dev/null +++ b/AuthenticatorSyncKit/Info.plist @@ -0,0 +1,18 @@ + + + + + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleName + AuthenticatorSyncKit + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/AuthenticatorSyncKit/SharedKeychainRepository.swift b/AuthenticatorSyncKit/SharedKeychainRepository.swift new file mode 100644 index 000000000..73c96d6de --- /dev/null +++ b/AuthenticatorSyncKit/SharedKeychainRepository.swift @@ -0,0 +1,155 @@ +import Foundation + +// MARK: - SharedKeychainItem + +/// Enumeration of support Keychain Items that can be placed in the `SharedKeychainRepository` +/// +public enum SharedKeychainItem: Equatable { + /// The keychain item for the authenticator encryption key. + case authenticatorKey + + /// The storage key for this keychain item. + /// + var unformattedKey: String { + switch self { + case .authenticatorKey: + "authenticatorKey" + } + } +} + +// MARK: - SharedKeychainRepository + +/// A repository for managing keychain items to be shared between the main Bitwarden app and the Authenticator app. +/// +public protocol SharedKeychainRepository: AnyObject { + /// Attempts to delete the authenticator key from the keychain. + /// + func deleteAuthenticatorKey() throws + + /// Gets the authenticator key. + /// + /// - Returns: Data representing the authenticator key. + /// + func getAuthenticatorKey() async throws -> Data + + /// Stores the access token for a user in the keychain. + /// + /// - Parameter value: The authenticator key to store. + /// + func setAuthenticatorKey(_ value: Data) async throws +} + +// MARK: - DefaultKeychainRepository + +/// A concreate implementation of the `SharedKeychainRepository` protocol. +/// +public class DefaultSharedKeychainRepository: SharedKeychainRepository { + // MARK: Properties + + /// An identifier for the shared access group used by the application. + /// + /// Example: "group.com.8bit.bitwarden" + /// + private let sharedAppGroupIdentifier: String + + /// The keychain service used by the repository + /// + private let keychainService: AuthenticatorKeychainService + + // MARK: Initialization + + /// Initialize a `DefaultSharedKeychainRepository`. + /// + /// - Parameters: + /// - sharedAppGroupIdentifier: An identifier for the shared access group used by the application. + /// - keychainService: The keychain service used by the repository + public init( + sharedAppGroupIdentifier: String, + keychainService: AuthenticatorKeychainService + ) { + self.sharedAppGroupIdentifier = sharedAppGroupIdentifier + self.keychainService = keychainService + } + + // MARK: Methods + + /// Retrieve the value for the specific item from the Keychain Service. + /// + /// - Parameter item: the keychain item for which to retrieve a value. + /// - Returns: The value (Data) stored in the keychain for the given item. + /// + private func getSharedValue(for item: SharedKeychainItem) async throws -> Data { + let foundItem = try keychainService.search( + query: [ + kSecMatchLimit: kSecMatchLimitOne, + kSecReturnData: true, + kSecReturnAttributes: true, + kSecAttrAccessGroup: sharedAppGroupIdentifier, + kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + kSecAttrAccount: item.unformattedKey, + kSecClass: kSecClassGenericPassword, + ] as CFDictionary + ) + + guard let resultDictionary = foundItem as? [String: Any], + let data = resultDictionary[kSecValueData as String] as? Data else { + throw AuthenticatorKeychainServiceError.keyNotFound(item) + } + + return data + } + + /// Store a given value into the keychain for the given item. + /// + /// - Parameters: + /// - value: The value (Data) to be stored into the keychain + /// - item: The item for which to store the value in the keychain. + /// + private func setSharedValue(_ value: Data, for item: SharedKeychainItem) async throws { + let query = [ + kSecValueData: value, + kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + kSecAttrAccessGroup: sharedAppGroupIdentifier, + kSecAttrAccount: item.unformattedKey, + kSecClass: kSecClassGenericPassword, + ] as CFDictionary + + try? keychainService.delete(query: query) + + try keychainService.add( + attributes: query + ) + } +} + +public extension DefaultSharedKeychainRepository { + /// Attempts to delete the authenticator key from the keychain. + /// + func deleteAuthenticatorKey() throws { + try keychainService.delete( + query: [ + kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + kSecAttrAccessGroup: sharedAppGroupIdentifier, + kSecAttrAccount: SharedKeychainItem.authenticatorKey.unformattedKey, + kSecClass: kSecClassGenericPassword, + ] as CFDictionary + ) + } + + /// Gets the authenticator key. + /// + /// - Returns: Data representing the authenticator key. + /// + func getAuthenticatorKey() async throws -> Data { + try await getSharedValue(for: .authenticatorKey) + } + + /// Stores the access token for a user in the keychain. + /// + /// - Parameter value: The authenticator key to store. + /// + func setAuthenticatorKey(_ value: Data) async throws { + try await setSharedValue(value, for: .authenticatorKey) + } +} diff --git a/AuthenticatorSyncKit/Tests/SharedKeychainRepositoryTests.swift b/AuthenticatorSyncKit/Tests/SharedKeychainRepositoryTests.swift new file mode 100644 index 000000000..fcf497dbd --- /dev/null +++ b/AuthenticatorSyncKit/Tests/SharedKeychainRepositoryTests.swift @@ -0,0 +1,126 @@ +import CryptoKit +import Foundation +import XCTest + +@testable import AuthenticatorSyncKit + +final class SharedKeychainRepositoryTests: AuthenticatorSyncKitTestCase { + // MARK: Properties + + let accessGroup = "group.com.example.bitwarden" + var keychainService: MockAuthenticatorKeychainService! + var subject: DefaultSharedKeychainRepository! + + // MARK: Setup & Teardown + + override func setUp() { + keychainService = MockAuthenticatorKeychainService() + subject = DefaultSharedKeychainRepository( + sharedAppGroupIdentifier: accessGroup, + keychainService: keychainService + ) + } + + override func tearDown() { + keychainService = nil + subject = nil + } + + // MARK: Tests + + /// Verify that `deleteAuthenticatorKey()` issues a delete with the correct search attributes specified. + /// + func test_deleteAuthenticatorKey_success() async throws { + try subject.deleteAuthenticatorKey() + + let queries = try XCTUnwrap(keychainService.deleteQueries as? [[CFString: Any]]) + XCTAssertEqual(queries.count, 1) + + let query = try XCTUnwrap(queries.first) + try XCTAssertEqual(XCTUnwrap(query[kSecAttrAccessGroup] as? String), accessGroup) + try XCTAssertEqual(XCTUnwrap(query[kSecAttrAccessible] as? String), + String(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)) + try XCTAssertEqual(XCTUnwrap(query[kSecAttrAccount] as? String), + SharedKeychainItem.authenticatorKey.unformattedKey) + try XCTAssertEqual(XCTUnwrap(query[kSecClass] as? String), + String(kSecClassGenericPassword)) + } + + /// Verify that `getAuthenticatorKey()` returns a value successfully when one is set. Additionally, verify the + /// search attributes are specified correctly. + /// + func test_getAuthenticatorKey_success() async throws { + let key = SymmetricKey(size: .bits256) + let data = key.withUnsafeBytes { Data(Array($0)) } + + keychainService.setSearchResultData(data) + + let returnData = try await subject.getAuthenticatorKey() + XCTAssertEqual(returnData, data) + + let query = try XCTUnwrap(keychainService.searchQuery as? [CFString: Any]) + try XCTAssertEqual(XCTUnwrap(query[kSecAttrAccessGroup] as? String), accessGroup) + try XCTAssertEqual(XCTUnwrap(query[kSecAttrAccessible] as? String), + String(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)) + try XCTAssertEqual(XCTUnwrap(query[kSecAttrAccount] as? String), + SharedKeychainItem.authenticatorKey.unformattedKey) + try XCTAssertEqual(XCTUnwrap(query[kSecClass] as? String), String(kSecClassGenericPassword)) + try XCTAssertEqual(XCTUnwrap(query[kSecMatchLimit] as? String), String(kSecMatchLimitOne)) + try XCTAssertTrue(XCTUnwrap(query[kSecReturnAttributes] as? Bool)) + try XCTAssertTrue(XCTUnwrap(query[kSecReturnData] as? Bool)) + } + + /// Verify that `getAuthenticatorKey()` fails with a `keyNotFound` error when an unexpected + /// result is returned instead of the key data from the keychain + /// + func test_getAuthenticatorKey_badResult() async throws { + let error = AuthenticatorKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey) + keychainService.searchResult = .success([kSecValueData as String: NSObject()] as AnyObject) + + await assertAsyncThrows(error: error) { + _ = try await subject.getAuthenticatorKey() + } + } + + /// Verify that `getAuthenticatorKey()` fails with a `keyNotFound` error when a nil + /// result is returned instead of the key data from the keychain + /// + func test_getAuthenticatorKey_nilResult() async throws { + let error = AuthenticatorKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey) + keychainService.searchResult = .success(nil) + + await assertAsyncThrows(error: error) { + _ = try await subject.getAuthenticatorKey() + } + } + + /// Verify that `getAuthenticatorKey()` fails with an error when the Authenticator key is not + /// present in the keychain + /// + func test_getAuthenticatorKey_keyNotFound() async throws { + let error = AuthenticatorKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey) + keychainService.searchResult = .failure(error) + + await assertAsyncThrows(error: error) { + _ = try await subject.getAuthenticatorKey() + } + } + + /// Verify that `setAuthenticatorKey(_:)` sets a value with the correct search attributes specified. + /// + func test_setAuthenticatorKey_success() async throws { + let key = SymmetricKey(size: .bits256) + let data = key.withUnsafeBytes { Data(Array($0)) } + try await subject.setAuthenticatorKey(data) + + let attributes = try XCTUnwrap(keychainService.addAttributes as? [CFString: Any]) + try XCTAssertEqual(XCTUnwrap(attributes[kSecAttrAccessGroup] as? String), accessGroup) + try XCTAssertEqual(XCTUnwrap(attributes[kSecAttrAccessible] as? String), + String(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)) + try XCTAssertEqual(XCTUnwrap(attributes[kSecAttrAccount] as? String), + SharedKeychainItem.authenticatorKey.unformattedKey) + try XCTAssertEqual(XCTUnwrap(attributes[kSecClass] as? String), + String(kSecClassGenericPassword)) + try XCTAssertEqual(XCTUnwrap(attributes[kSecValueData] as? Data), data) + } +} diff --git a/AuthenticatorSyncKit/Tests/TestHelpers/MockAuthenticatorKeychainService.swift b/AuthenticatorSyncKit/Tests/TestHelpers/MockAuthenticatorKeychainService.swift new file mode 100644 index 000000000..db905b4e6 --- /dev/null +++ b/AuthenticatorSyncKit/Tests/TestHelpers/MockAuthenticatorKeychainService.swift @@ -0,0 +1,40 @@ +import Foundation + +@testable import AuthenticatorSyncKit + +class MockAuthenticatorKeychainService { + // MARK: Properties + + var addAttributes: CFDictionary? + var addResult: Result = .success(()) + var deleteQueries = [CFDictionary]() + var deleteResult: Result = .success(()) + var searchQuery: CFDictionary? + var searchResult: Result = .success(nil) +} + +// MARK: KeychainService + +extension MockAuthenticatorKeychainService: AuthenticatorKeychainService { + func add(attributes: CFDictionary) throws { + addAttributes = attributes + try addResult.get() + } + + func delete(query: CFDictionary) throws { + deleteQueries.append(query) + try deleteResult.get() + } + + func search(query: CFDictionary) throws -> AnyObject? { + searchQuery = query + return try searchResult.get() + } +} + +extension MockAuthenticatorKeychainService { + func setSearchResultData(_ data: Data) { + let dictionary = [kSecValueData as String: data] + searchResult = .success(dictionary as AnyObject) + } +} diff --git a/AuthenticatorSyncKit/Tests/TestHelpers/MockSharedKeychainRepository.swift b/AuthenticatorSyncKit/Tests/TestHelpers/MockSharedKeychainRepository.swift new file mode 100644 index 000000000..394ec002a --- /dev/null +++ b/AuthenticatorSyncKit/Tests/TestHelpers/MockSharedKeychainRepository.swift @@ -0,0 +1,31 @@ +import CryptoKit +import Foundation + +@testable import AuthenticatorSyncKit + +class MockSharedKeychainRepository { + var authenticatorKey: Data? +} + +extension MockSharedKeychainRepository: SharedKeychainRepository { + func generateKeyData() -> Data { + let key = SymmetricKey(size: .bits256) + return key.withUnsafeBytes { Data(Array($0)) } + } + + func deleteAuthenticatorKey() throws { + authenticatorKey = nil + } + + func getAuthenticatorKey() async throws -> Data { + if let authenticatorKey { + return authenticatorKey + } else { + throw AuthenticatorKeychainServiceError.keyNotFound(.authenticatorKey) + } + } + + func setAuthenticatorKey(_ value: Data) async throws { + authenticatorKey = value + } +} diff --git a/AuthenticatorSyncKit/Tests/TestHelpers/Support/AuthenticatorSyncKitTestCase.swift b/AuthenticatorSyncKit/Tests/TestHelpers/Support/AuthenticatorSyncKitTestCase.swift new file mode 100644 index 000000000..2df928057 --- /dev/null +++ b/AuthenticatorSyncKit/Tests/TestHelpers/Support/AuthenticatorSyncKitTestCase.swift @@ -0,0 +1,198 @@ +import XCTest + +/// Base class for any tests in the AuthenticatorSyncKit framework. +/// +open class AuthenticatorSyncKitTestCase: XCTestCase { + /// Asserts that an asynchronous block of code will throw an error. The test will fail if the + /// block does not throw an error. + /// + /// - Note: This method does not rethrow the error thrown by `block`. + /// + /// - Parameters: + /// - block: The block to be executed. This block is run asynchronously. + /// - file: The file in which the failure occurred. Defaults to the file name of the test + /// case in which the function was called from. + /// - line: The line number in which the failure occurred. Defaults to the line number on + /// which this function was called from. + /// + open func assertAsyncThrows( + _ block: () async throws -> Void, + file: StaticString = #file, + line: UInt = #line + ) async { + do { + try await block() + XCTFail("The block did not throw an error.", file: file, line: line) + } catch {} + } + + /// Asserts that an asynchronous block of code will throw a specific error. The test will fail + /// if the block does not throw an error or if the error thrown does not equal the provided error. + /// + /// - Note: This method does not rethrow the error thrown by `block`. + /// + /// - Parameters: + /// - error: The specific error that must be thrown by `block`. + /// - block: The block to be executed. This block is run asynchronously. + /// - file: The file in which the failure occurred. Defaults to the file name of the test + /// case in which the function was called from. + /// - line: The line number in which the failure occurred. Defaults to the line number on + /// which this function was called from. + /// + open func assertAsyncThrows( + error: E, + file: StaticString = #file, + line: UInt = #line, + _ block: () async throws -> Void + ) async { + do { + try await block() + XCTFail("The block did not throw an error.", file: file, line: line) + } catch let caughtError as E { + XCTAssertEqual(caughtError, error, file: file, line: line) + } catch let caughtError { + XCTFail( + "The error caught (\(caughtError)) does not match the type of error provided (\(error)).", + file: file, + line: line + ) + } + } + + /// Asserts that an asynchronous block of code does not throw an error. The test will fail + /// if the block throws an error. + /// + /// - Parameters: + /// - block: The block to be executed. This block is run asynchronously. + /// - file: The file in which the failure occurred. Defaults to the file name of the test + /// case in which the function was called from. + /// - line: The line number in which the failure occurred. Defaults to the line number on + /// which this function was called from. + /// + open func assertAsyncDoesNotThrow( + _ block: () async throws -> Void, + file: StaticString = #file, + line: UInt = #line + ) async { + do { + try await block() + } catch { + XCTFail("The block threw an error.", file: file, line: line) + } + } + + /// Wait for a condition to be true. The test will fail if the condition isn't met before the + /// specified timeout. + /// + /// - Parameters: + /// - condition: Return `true` to continue or `false` to keep waiting. + /// - timeout: How long to wait before failing. + /// - failureMessage: Message to display when the condition fails to be met. + /// - file: The file in which the failure occurred. Defaults to the file name of the test + /// case in which the function was called from. + /// - line: The line number in which the failure occurred. Defaults to the line number on + /// which this function was called from. + /// + open func waitFor( + _ condition: () -> Bool, + timeout: TimeInterval = 10.0, + failureMessage: String = "waitFor condition wasn't met within the time limit", + file: StaticString = #file, + line: UInt = #line + ) { + let start = Date() + let limit = Date(timeIntervalSinceNow: timeout) + + while !condition(), limit > Date() { + let next = Date(timeIntervalSinceNow: 0.2) + RunLoop.current.run(mode: RunLoop.Mode.default, before: next) + } + + warnIfNeeded(start: start, line: line) + + XCTAssert(condition(), failureMessage, file: file, line: line) + } + + /// Wait for a condition to be true. The test will fail if the condition isn't met before the + /// specified timeout. + /// + /// - Parameters: + /// - condition: An expression that evaluates to `true` to continue or `false` to keep waiting. + /// - timeout: How long to wait before failing. + /// - failureMessage: Message to display when the condition fails to be met. + /// - file: The file in which the failure occurred. Defaults to the file name of the test + /// case in which the function was called from. + /// - line: The line number in which the failure occurred. Defaults to the line number on + /// which this function was called from. + /// + open func waitFor( + _ condition: @autoclosure () -> Bool, + timeout: TimeInterval = 10.0, + failureMessage: String = "waitFor condition wasn't met within the time limit", + file: StaticString = #file, + line: UInt = #line + ) { + waitFor( + condition, + timeout: timeout, + failureMessage: failureMessage, + file: file, + line: line + ) + } + + /// Wait for a condition asynchronously to be true. The test will fail if the condition isn't met before the + /// specified timeout. + /// + /// - Parameters: + /// - condition: Return `true` to continue or `false` to keep waiting. + /// - timeout: How long to wait before failing. + /// - failureMessage: Message to display when the condition fails to be met. + /// - file: The file in which the failure occurred. Defaults to the file name of the test + /// case in which the function was called from. + /// - line: The line number in which the failure occurred. Defaults to the line number on + /// which this function was called from. + /// + open func waitForAsync( + _ condition: @escaping () -> Bool, + timeout: TimeInterval = 10.0, + failureMessage: String = "waitForAsync condition wasn't met within the time limit", + file: StaticString = #file, + line: UInt = #line + ) async throws { + let start = Date() + let limit = Date(timeIntervalSinceNow: timeout) + + while !condition(), limit > Date() { + try await Task.sleep(nanoseconds: 2 * 100_000_000) + } + + warnIfNeeded(start: start, line: line) + + XCTAssert(condition(), failureMessage, file: file, line: line) + } + + /// Warns if `functionName` took more than `afterSeconds` to complete + /// - Parameters: + /// - start: When `waitFor` started + /// - afterSeconds: The seconds that have passed since `start` to check against + /// - functionName: The function name + /// - line: File line were this was originated + private func warnIfNeeded( + start: Date, + afterSeconds: Int = 3, + functionName: String = #function, + line: UInt = #line + ) { + // If the condition took more than 3 seconds to satisfy, add a warning to the logs to look into it. + let elapsed = Date().timeIntervalSince(start) + if elapsed > 3 { + let numberFormatter = NumberFormatter() + numberFormatter.maximumFractionDigits = 3 + numberFormatter.minimumFractionDigits = 3 + numberFormatter.minimumIntegerDigits = 1 + let elapsedString: String = numberFormatter.string(from: NSNumber(value: elapsed)) ?? "nil" + print("warning: \(name) line \(line) `\(functionName)` took \(elapsedString) seconds") + } + } +} diff --git a/AuthenticatorSyncKit/Tests/TestHelpers/Support/Info.plist b/AuthenticatorSyncKit/Tests/TestHelpers/Support/Info.plist new file mode 100644 index 000000000..7c1df8d12 --- /dev/null +++ b/AuthenticatorSyncKit/Tests/TestHelpers/Support/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 0.1.0 + CFBundleVersion + 1 + + diff --git a/BitwardenShared/Core/Auth/Services/KeychainService.swift b/BitwardenShared/Core/Auth/Services/KeychainService.swift index 196965bfa..3d62dff68 100644 --- a/BitwardenShared/Core/Auth/Services/KeychainService.swift +++ b/BitwardenShared/Core/Auth/Services/KeychainService.swift @@ -1,3 +1,4 @@ +import AuthenticatorSyncKit import Foundation // MARK: - KeychainService @@ -113,3 +114,7 @@ class DefaultKeychainService: KeychainService { } } } + +// MARK: - AuthenticatorKeychainService + +extension DefaultKeychainService: AuthenticatorKeychainService {} diff --git a/Configs/AuthenticatorSyncKit.xcconfig b/Configs/AuthenticatorSyncKit.xcconfig new file mode 100644 index 000000000..13f6dd5ec --- /dev/null +++ b/Configs/AuthenticatorSyncKit.xcconfig @@ -0,0 +1,4 @@ +#include "./Common.xcconfig" +#include? "./Local.xcconfig" + +PRODUCT_BUNDLE_IDENTIFIER = $(ORGANIZATION_IDENTIFIER).authenticator-sync-kit diff --git a/Package.swift b/Package.swift new file mode 100644 index 000000000..864fb5968 --- /dev/null +++ b/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version: 5.7 + +import PackageDescription + +let package = Package( + name: "BitwardenShared", + platforms: [ + .iOS(.v15), + ], + products: [ + .library( + name: "AuthenticatorSyncKit", + targets: [ + "AuthenticatorSyncKit", + ] + ), + ], + targets: [ + .target( + name: "AuthenticatorSyncKit", + path: "AuthenticatorSyncKit", + exclude: [ + "Tests/", + ] + ), + .testTarget( + name: "AuthenticatorSyncKitTests", + dependencies: ["AuthenticatorSyncKit"], + path: "AuthenticatorSyncKit/Tests/" + ), + ] +) diff --git a/project.yml b/project.yml index bd1b6917b..ca3b49059 100644 --- a/project.yml +++ b/project.yml @@ -36,6 +36,15 @@ packages: url: https://github.com/nalexn/ViewInspector exactVersion: 0.9.10 schemes: + AuthenticatorSyncKit: + build: + targets: + AuthenticatorSyncKit: all + AuthenticatorSyncKitTests: [test] + test: + gatherCoverageData: true + targets: + - AuthenticatorSyncKitTests Bitwarden: build: targets: @@ -50,12 +59,14 @@ schemes: language: en region: US coverageTargets: + - AuthenticatorSyncKit - Bitwarden - BitwardenActionExtension - BitwardenAutoFillExtension - BitwardenShareExtension - BitwardenShared targets: + - AuthenticatorSyncKitTests - BitwardenTests - BitwardenActionExtensionTests - BitwardenAutoFillExtensionTests @@ -125,6 +136,33 @@ schemes: targets: BitwardenWatchWidgetExtension: all targets: + AuthenticatorSyncKit: + type: framework + platform: iOS + configFiles: + Debug: Configs/AuthenticatorSyncKit.xcconfig + Release: Configs/AuthenticatorSyncKit.xcconfig + settings: + base: + APPLICATION_EXTENSION_API_ONLY: true + INFOPLIST_FILE: AuthenticatorSyncKit/Info.plist + sources: + - path: AuthenticatorSyncKit + excludes: + - "**/Tests/*" + AuthenticatorSyncKitTests: + type: bundle.unit-test + platform: iOS + settings: + base: + INFOPLIST_FILE: AuthenticatorSyncKit/Tests/TestHelpers/Support/Info.plist + sources: + - path: AuthenticatorSyncKit + includes: + - "**/Tests/*" + dependencies: + - target: AuthenticatorSyncKit + randomExecutionOrder: true Bitwarden: type: application platform: iOS @@ -154,6 +192,7 @@ targets: - path: swiftgen.yml buildPhase: none dependencies: + - target: AuthenticatorSyncKit - target: BitwardenShared - target: BitwardenActionExtension - target: BitwardenAutoFillExtension @@ -206,6 +245,7 @@ targets: - "**/TestHelpers/*" - path: GlobalTestHelpers dependencies: + - target: AuthenticatorSyncKit - target: Bitwarden - target: BitwardenShared - package: SnapshotTesting @@ -351,6 +391,7 @@ targets: optional: true - path: BitwardenWatchShared dependencies: + - target: AuthenticatorSyncKit - package: BitwardenSdk - package: Networking - package: SwiftUIIntrospect