[PM-21681] Build out Shared Keychain functionality (#1636)

This commit is contained in:
Katherine Bertelsen 2025-06-10 12:03:49 -05:00 committed by GitHub
parent e0aa268b98
commit b609b053c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 462 additions and 232 deletions

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View File

@ -12,7 +12,7 @@ public protocol SharedCryptographyService: AnyObject {
///
/// - Parameter items: The encrypted array of items to be decrypted
/// - Returns: the array of items with their data decrypted
/// - Throws: AuthenticatorKeychainServiceError.keyNotFound if the Authenticator
/// - Throws: SharedKeychainServiceError.keyNotFound if the Authenticator
/// key is not in the shared repository.
///
func decryptAuthenticatorItems(
@ -24,7 +24,7 @@ public protocol SharedCryptographyService: AnyObject {
///
/// - Parameter items: The decrypted array of items to be encrypted
/// - Returns: the array of items with their data encrypted
/// - Throws: AuthenticatorKeychainServiceError.keyNotFound if the Authenticator
/// - Throws: SharedKeychainServiceError.keyNotFound if the Authenticator
/// key is not in the shared repository.
///
func encryptAuthenticatorItems(

View File

@ -0,0 +1,53 @@
import AuthenticatorBridgeKit
import BitwardenKit
import CryptoKit
import Foundation
public class MockSharedKeychainRepository: SharedKeychainRepository {
public var authenticatorKey: Data?
public var errorToThrow: Error?
public var accountAutoLogoutTime = [String: Date]()
public init() {}
/// Generates a `Data` object that looks like the key used for encrypting shared items.
/// Useful for tests that want reasonably authentic-looking data.
public func generateMockKeyData() -> Data {
let key = SymmetricKey(size: .bits256)
return key.withUnsafeBytes { Data(Array($0)) }
}
public func deleteAuthenticatorKey() throws {
if let errorToThrow { throw errorToThrow }
authenticatorKey = nil
}
public func getAuthenticatorKey() async throws -> Data {
if let errorToThrow { throw errorToThrow }
if let authenticatorKey {
return authenticatorKey
} else {
throw SharedKeychainServiceError.keyNotFound(.authenticatorKey)
}
}
public func setAuthenticatorKey(_ value: Data) async throws {
if let errorToThrow { throw errorToThrow }
authenticatorKey = value
}
public func getAccountAutoLogoutTime(userId: String) async throws -> Date? {
if let errorToThrow { throw errorToThrow }
return accountAutoLogoutTime[userId]
}
public func setAccountAutoLogoutTime(_ value: Date?, userId: String) async throws {
if let errorToThrow { throw errorToThrow }
accountAutoLogoutTime[userId] = value
}
}

View File

@ -0,0 +1,35 @@
import AuthenticatorBridgeKit
import Foundation
public class MockSharedKeychainService: SharedKeychainService {
// MARK: Properties
public var addAttributes: CFDictionary?
public var addResult: Result<Void, SharedKeychainServiceError> = .success(())
public var deleteQueries = [CFDictionary]()
public var deleteResult: Result<Void, SharedKeychainServiceError> = .success(())
public var searchQuery: CFDictionary?
public var searchResult: Result<AnyObject?, SharedKeychainServiceError> = .success(nil)
public init() {}
public func add(attributes: CFDictionary) throws {
addAttributes = attributes
try addResult.get()
}
public func delete(query: CFDictionary) throws {
deleteQueries.append(query)
try deleteResult.get()
}
public func search(query: CFDictionary) throws -> AnyObject? {
searchQuery = query
return try searchResult.get()
}
public func setSearchResultData(_ data: Data) {
let dictionary = [kSecValueData as String: data]
searchResult = .success(dictionary as AnyObject)
}
}

View File

@ -0,0 +1,23 @@
import AuthenticatorBridgeKit
import Foundation
public class MockSharedKeychainStorage: SharedKeychainStorage {
public var storage = [SharedKeychainItem: any Codable]()
public init() {}
public func deleteValue(for item: SharedKeychainItem) async throws {
storage[item] = nil
}
public func getValue<T>(for item: SharedKeychainItem) async throws -> T where T: Codable {
guard let stored = storage[item] as? T else {
throw SharedKeychainServiceError.keyNotFound(item)
}
return stored
}
public func setValue<T>(_ value: T, for item: SharedKeychainItem) async throws where T: Codable {
storage[item] = value
}
}

View File

@ -0,0 +1,101 @@
import BitwardenKit
import Foundation
// MARK: - SharedKeychainRepository
/// A repository for managing keychain items to be shared between Password Manager and Authenticator.
/// This should be the entry point in retrieving items from the shared keychain.
public protocol SharedKeychainRepository {
/// Deletes the authenticator key.
///
func deleteAuthenticatorKey() async 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
/// Gets when a user account should automatically log out.
///
/// - Parameters:
/// - userId: The user ID of the account
/// - Returns: The time the user should be automatically logged out. If `nil`, then the user should not be.
///
func getAccountAutoLogoutTime(
userId: String
) async throws -> Date?
/// Sets when a user account should automatically log out.
///
/// - Parameters:
/// - value: when the user should be automatically logged out
/// - userId: The user ID of the account
///
func setAccountAutoLogoutTime(
_ value: Date?,
userId: String
) async throws
}
public class DefaultSharedKeychainRepository: SharedKeychainRepository {
/// The shared keychain storage used by the repository.
let storage: SharedKeychainStorage
/// Initialize a `DefaultSharedKeychainStorage`.
///
/// - Parameters:
/// - storage: The shared keychain storage used by the repository
public init(storage: SharedKeychainStorage) {
self.storage = storage
}
public func deleteAuthenticatorKey() async throws {
try await storage.deleteValue(for: .authenticatorKey)
}
/// Gets the authenticator key.
///
/// - Returns: Data representing the authenticator key.
///
public func getAuthenticatorKey() async throws -> Data {
try await storage.getValue(for: .authenticatorKey)
}
/// Stores the access token for a user in the keychain.
///
/// - Parameter value: The authenticator key to store.
///
public func setAuthenticatorKey(_ value: Data) async throws {
try await storage.setValue(value, for: .authenticatorKey)
}
/// Gets when a user account should automatically log out.
///
/// - Parameters:
/// - userId: The user ID of the account
/// - Returns: The time the user should be automatically logged out. If `nil`, then the user should not be.
///
public func getAccountAutoLogoutTime(userId: String) async throws -> Date? {
try await storage.getValue(for: .accountAutoLogout(userId: userId))
}
/// Sets when a user account should automatically log out.
///
/// - Parameters:
/// - value: when the user should be automatically logged out
/// - userId: The user ID of the account
///
public func setAccountAutoLogoutTime(
_ value: Date?,
userId: String
) async throws {
try await storage.setValue(value, for: .accountAutoLogout(userId: userId))
}
}

View File

@ -0,0 +1,76 @@
import AuthenticatorBridgeKit
import AuthenticatorBridgeKitMocks
import BitwardenKit
import BitwardenKitMocks
import CryptoKit
import Foundation
import XCTest
final class SharedKeychainRepositoryTests: BitwardenTestCase {
// MARK: Properties
var storage: MockSharedKeychainStorage!
var subject: DefaultSharedKeychainRepository!
// MARK: Setup & Teardown
override func setUp() {
storage = MockSharedKeychainStorage()
subject = DefaultSharedKeychainRepository(
storage: storage
)
}
override func tearDown() {
storage = nil
subject = nil
}
// MARK: Tests
/// `deleteAuthenticatorKey()` deletes the authenticator key from storage.
func test_deleteAuthenticatorKey_success() async throws {
storage.storage[.authenticatorKey] = Data()
try await subject.deleteAuthenticatorKey()
XCTAssertNil(storage.storage[.authenticatorKey])
}
/// `getAuthenticatorKey()` retrieves the authenticator key from storage.
func test_getAuthenticatorKey_success() async throws {
let key = SymmetricKey(size: .bits256)
let data = key.withUnsafeBytes { Data(Array($0)) }
storage.storage[.authenticatorKey] = data
let authenticatorKey = try await subject.getAuthenticatorKey()
XCTAssertEqual(authenticatorKey, data)
}
/// `getAuthenticatorKey()` throws an error if the key is not in storage.
func test_getAuthenticatorKey_nil() async throws {
await assertAsyncThrows(error: SharedKeychainServiceError.keyNotFound(.authenticatorKey)) {
_ = try await subject.getAuthenticatorKey()
}
}
/// `setAuthenticatorKey()` sets the authenticator key in storage.
func test_setAuthenticatorKey_success() async throws {
let key = SymmetricKey(size: .bits256)
let data = key.withUnsafeBytes { Data(Array($0)) }
try await subject.setAuthenticatorKey(data)
XCTAssertEqual(storage.storage[.authenticatorKey] as? Data, data)
}
/// `getAccountAutoLogoutTime()` retrieves the last active time from storage.
func test_getPMAccountAutoLogoutTime_success() async throws {
let date = Date(timeIntervalSince1970: 12345)
storage.storage[.accountAutoLogout(userId: "1")] = date
let lastActiveTime = try await subject.getAccountAutoLogoutTime(userId: "1")
XCTAssertEqual(lastActiveTime, date)
}
/// `setAccountAutoLogoutTime()` sets the last active time in storage.
func test_setPMAccountAutoLogoutTime_success() async throws {
let date = Date(timeIntervalSince1970: 12345)
try await subject.setAccountAutoLogoutTime(date, userId: "1")
XCTAssertEqual(storage.storage[.accountAutoLogout(userId: "1")] as? Date, date)
}
}

View File

@ -1,11 +1,11 @@
import Foundation
// MARK: - AuthenticatorKeychainService
// MARK: - SharedKeychainService
/// 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 {
public protocol SharedKeychainService: AnyObject {
/// Adds a set of attributes.
///
/// - Parameter attributes: Attributes to add.
@ -26,10 +26,10 @@ public protocol AuthenticatorKeychainService: AnyObject {
func search(query: CFDictionary) throws -> AnyObject?
}
// MARK: - AuthenticatorKeychainServiceError
// MARK: - SharedKeychainServiceError
/// Enum with possible error cases that can be thrown from `AuthenticatorKeychainService`.
public enum AuthenticatorKeychainServiceError: Error, Equatable, CustomNSError {
/// Enum with possible error cases that can be thrown from `SharedKeychainService`.
public enum SharedKeychainServiceError: Error, Equatable, CustomNSError {
/// 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.

View File

@ -4,82 +4,96 @@ import Foundation
/// Enumeration of support Keychain Items that can be placed in the `SharedKeychainRepository`
///
public enum SharedKeychainItem: Equatable {
public enum SharedKeychainItem: Equatable, Hashable {
/// The keychain item for the authenticator encryption key.
case authenticatorKey
/// A date at which a PM account automatically logs out.
case accountAutoLogout(userId: String)
/// The storage key for this keychain item.
///
var unformattedKey: String {
public var unformattedKey: String {
switch self {
case .authenticatorKey:
"authenticatorKey"
case let .accountAutoLogout(userId: userId):
"accountAutoLogout_\(userId)"
}
}
}
// MARK: - SharedKeychainRepository
/// A storage layer for managing keychain items that are shared between Password Manager
/// and Authenticator. In particular, it is able to construct the appropriate queries to
/// talk with a `SharedKeychainService`.
///
public protocol SharedKeychainStorage {
/// Deletes the value in 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.
///
func deleteValue(for item: SharedKeychainItem) async throws
/// A repository for managing keychain items to be shared between the main Bitwarden app and the Authenticator app.
/// Retrieve the value for the specific item from the Keychain Service.
///
public protocol SharedKeychainRepository: AnyObject {
/// Attempts to delete the authenticator key from the keychain.
/// - Parameter item: the keychain item for which to retrieve a value.
/// - Returns: The value (Data) stored in the keychain for the given item.
///
func deleteAuthenticatorKey() throws
func getValue<T: Codable>(for item: SharedKeychainItem) async throws -> T
/// Gets the authenticator key.
/// Store a given value into the keychain for the given item.
///
/// - Returns: Data representing the authenticator key.
/// - Parameters:
/// - value: The value (Data) to be stored into the keychain
/// - item: The item for which to store the value in the keychain.
///
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
func setValue<T: Codable>(_ value: T, for item: SharedKeychainItem) async throws
}
// MARK: - DefaultKeychainRepository
/// A concrete implementation of the `SharedKeychainRepository` protocol.
///
public class DefaultSharedKeychainRepository: SharedKeychainRepository {
public class DefaultSharedKeychainStorage: SharedKeychainStorage {
// MARK: Properties
/// The keychain service used by the repository
///
private let keychainService: SharedKeychainService
/// 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`.
/// Initialize a `DefaultSharedKeychainStorage`.
///
/// - Parameters:
/// - sharedAppGroupIdentifier: An identifier for the shared access group used by the application.
/// - keychainService: The keychain service used by the repository
/// - sharedAppGroupIdentifier: An identifier for the shared access group used by the application.
public init(
sharedAppGroupIdentifier: String,
keychainService: AuthenticatorKeychainService
keychainService: SharedKeychainService,
sharedAppGroupIdentifier: String
) {
self.sharedAppGroupIdentifier = sharedAppGroupIdentifier
self.keychainService = keychainService
self.sharedAppGroupIdentifier = sharedAppGroupIdentifier
}
// 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 {
public func deleteValue(for item: SharedKeychainItem) async throws {
try keychainService.delete(
query: [
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
kSecAttrAccessGroup: sharedAppGroupIdentifier,
kSecAttrAccount: item.unformattedKey,
kSecClass: kSecClassGenericPassword,
] as CFDictionary
)
}
public func getValue<T: Codable>(for item: SharedKeychainItem) async throws -> T {
let foundItem = try keychainService.search(
query: [
kSecMatchLimit: kSecMatchLimitOne,
@ -93,20 +107,14 @@ public class DefaultSharedKeychainRepository: SharedKeychainRepository {
)
guard let resultDictionary = foundItem as? [String: Any],
let data = resultDictionary[kSecValueData as String] as? Data else {
throw AuthenticatorKeychainServiceError.keyNotFound(item)
let data = resultDictionary[kSecValueData as String] as? T else {
throw SharedKeychainServiceError.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 {
public func setValue<T: Codable>(_ value: T, for item: SharedKeychainItem) async throws {
let query = [
kSecValueData: value,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
@ -122,34 +130,3 @@ public class DefaultSharedKeychainRepository: SharedKeychainRepository {
)
}
}
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)
}
}

View File

@ -1,23 +1,23 @@
import AuthenticatorBridgeKit
import AuthenticatorBridgeKitMocks
import CryptoKit
import Foundation
import XCTest
@testable import AuthenticatorBridgeKit
final class SharedKeychainRepositoryTests: AuthenticatorBridgeKitTestCase {
final class SharedKeychainStorageTests: BitwardenTestCase {
// MARK: Properties
let accessGroup = "group.com.example.bitwarden"
var keychainService: MockAuthenticatorKeychainService!
var subject: DefaultSharedKeychainRepository!
var keychainService: MockSharedKeychainService!
var subject: SharedKeychainStorage!
// MARK: Setup & Teardown
override func setUp() {
keychainService = MockAuthenticatorKeychainService()
subject = DefaultSharedKeychainRepository(
sharedAppGroupIdentifier: accessGroup,
keychainService: keychainService
keychainService = MockSharedKeychainService()
subject = DefaultSharedKeychainStorage(
keychainService: keychainService,
sharedAppGroupIdentifier: accessGroup
)
}
@ -28,10 +28,10 @@ final class SharedKeychainRepositoryTests: AuthenticatorBridgeKitTestCase {
// MARK: Tests
/// Verify that `deleteAuthenticatorKey()` issues a delete with the correct search attributes specified.
/// Verify that `deleteValue(for:)` issues a delete with the correct search attributes specified.
///
func test_deleteAuthenticatorKey_success() async throws {
try subject.deleteAuthenticatorKey()
func test_deleteValue_success() async throws {
try await subject.deleteValue(for: .authenticatorKey)
let queries = try XCTUnwrap(keychainService.deleteQueries as? [[CFString: Any]])
XCTAssertEqual(queries.count, 1)
@ -46,16 +46,16 @@ final class SharedKeychainRepositoryTests: AuthenticatorBridgeKitTestCase {
String(kSecClassGenericPassword))
}
/// Verify that `getAuthenticatorKey()` returns a value successfully when one is set. Additionally, verify the
/// Verify that `getValue(for:)` returns a value successfully when one is set. Additionally, verify the
/// search attributes are specified correctly.
///
func test_getAuthenticatorKey_success() async throws {
func test_getValue_success() async throws {
let key = SymmetricKey(size: .bits256)
let data = key.withUnsafeBytes { Data(Array($0)) }
keychainService.setSearchResultData(data)
let returnData = try await subject.getAuthenticatorKey()
let returnData: Data = try await subject.getValue(for: .authenticatorKey)
XCTAssertEqual(returnData, data)
let query = try XCTUnwrap(keychainService.searchQuery as? [CFString: Any])
@ -73,45 +73,47 @@ final class SharedKeychainRepositoryTests: AuthenticatorBridgeKitTestCase {
/// 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)
func test_getValue_badResult() async throws {
let key = SharedKeychainItem.accountAutoLogout(userId: "1")
let error = SharedKeychainServiceError.keyNotFound(key)
keychainService.searchResult = .success([kSecValueData as String: NSObject()] as AnyObject)
await assertAsyncThrows(error: error) {
_ = try await subject.getAuthenticatorKey()
let _: Data = try await subject.getValue(for: key)
}
}
/// Verify that `getAuthenticatorKey()` fails with a `keyNotFound` error when a nil
/// Verify that `getValue(for:)` 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)
func test_getValue_nilResult() async throws {
let key = SharedKeychainItem.accountAutoLogout(userId: "1")
let error = SharedKeychainServiceError.keyNotFound(key)
keychainService.searchResult = .success(nil)
await assertAsyncThrows(error: error) {
_ = try await subject.getAuthenticatorKey()
let _: Data = try await subject.getValue(for: key)
}
}
/// Verify that `getAuthenticatorKey()` fails with an error when the Authenticator key is not
/// Verify that `getValue(for:)` 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)
let error = SharedKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
keychainService.searchResult = .failure(error)
await assertAsyncThrows(error: error) {
_ = try await subject.getAuthenticatorKey()
let _: Data = try await subject.getValue(for: .authenticatorKey)
}
}
/// Verify that `setAuthenticatorKey(_:)` sets a value with the correct search attributes specified.
/// Verify that `setValue(_:for:)` 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)
try await subject.setValue(data, for: .authenticatorKey)
let attributes = try XCTUnwrap(keychainService.addAttributes as? [CFString: Any])
try XCTAssertEqual(XCTUnwrap(attributes[kSecAttrAccessGroup] as? String), accessGroup)

View File

@ -1,3 +1,5 @@
import AuthenticatorBridgeKit
import AuthenticatorBridgeKitMocks
import BitwardenKit
import BitwardenKitMocks
import Foundation

View File

@ -1,3 +1,5 @@
import AuthenticatorBridgeKit
import AuthenticatorBridgeKitMocks
import BitwardenKit
import BitwardenKitMocks
import Foundation
@ -170,7 +172,7 @@ final class AuthenticatorBridgeItemServiceTests: AuthenticatorBridgeKitTestCase
/// Verify that `isSyncOn` returns true when the key is present in the keychain.
///
func test_isSyncOn_true() async throws {
let key = keychainRepository.generateKeyData()
let key = keychainRepository.generateMockKeyData()
try await keychainRepository.setAuthenticatorKey(key)
let sync = await subject.isSyncOn()
XCTAssertTrue(sync)

View File

@ -1,3 +1,5 @@
import AuthenticatorBridgeKit
import AuthenticatorBridgeKitMocks
import CryptoKit
import Foundation
import XCTest
@ -16,7 +18,7 @@ final class SharedCryptographyServiceTests: AuthenticatorBridgeKitTestCase {
override func setUp() {
super.setUp()
sharedKeychainRepository = MockSharedKeychainRepository()
sharedKeychainRepository.authenticatorKey = sharedKeychainRepository.generateKeyData()
sharedKeychainRepository.authenticatorKey = sharedKeychainRepository.generateMockKeyData()
subject = DefaultAuthenticatorCryptographyService(
sharedKeychainRepository: sharedKeychainRepository
)
@ -56,7 +58,7 @@ final class SharedCryptographyServiceTests: AuthenticatorBridgeKitTestCase {
///
func test_decryptAuthenticatorItems_throwsKeyMissingError() async throws {
let encryptedItems = try await subject.encryptAuthenticatorItems(items)
let error = AuthenticatorKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
let error = SharedKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
try sharedKeychainRepository.deleteAuthenticatorKey()
await assertAsyncThrows(error: error) {
@ -110,7 +112,7 @@ final class SharedCryptographyServiceTests: AuthenticatorBridgeKitTestCase {
/// when the `SharedKeyRepository` authenticator key is missing.
///
func test_encryptAuthenticatorItems_throwsKeyMissingError() async throws {
let error = AuthenticatorKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
let error = SharedKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
try sharedKeychainRepository.deleteAuthenticatorKey()
await assertAsyncThrows(error: error) {

View File

@ -1,40 +0,0 @@
import Foundation
@testable import AuthenticatorBridgeKit
class MockAuthenticatorKeychainService {
// MARK: Properties
var addAttributes: CFDictionary?
var addResult: Result<Void, AuthenticatorKeychainServiceError> = .success(())
var deleteQueries = [CFDictionary]()
var deleteResult: Result<Void, AuthenticatorKeychainServiceError> = .success(())
var searchQuery: CFDictionary?
var searchResult: Result<AnyObject?, AuthenticatorKeychainServiceError> = .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)
}
}

View File

@ -1,31 +0,0 @@
import CryptoKit
import Foundation
@testable import AuthenticatorBridgeKit
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
}
}

View File

@ -0,0 +1,9 @@
import TestHelpers
import XCTest
open class BitwardenTestCase: BaseBitwardenTestCase {
@MainActor
override open class func setUp() {
TestDataHelpers.defaultBundle = Bundle(for: Self.self)
}
}

View File

@ -1,4 +1,5 @@
import AuthenticatorBridgeKit
import BitwardenKit
import Foundation
// MARK: - KeychainService
@ -112,6 +113,6 @@ class DefaultKeychainService: KeychainService {
}
}
// MARK: - AuthenticatorKeychainService
// MARK: - SharedKeychainService
extension DefaultKeychainService: AuthenticatorKeychainService {}
extension DefaultKeychainService: SharedKeychainService {}

View File

@ -226,9 +226,13 @@ public class ServiceContainer: Services {
authenticatorItemDataStore: dataStore
)
let sharedKeychainStorage = DefaultSharedKeychainStorage(
keychainService: keychainService,
sharedAppGroupIdentifier: Bundle.main.sharedAppGroupIdentifier
)
let sharedKeychainRepository = DefaultSharedKeychainRepository(
sharedAppGroupIdentifier: Bundle.main.sharedAppGroupIdentifier,
keychainService: keychainService
storage: sharedKeychainStorage
)
let sharedCryptographyService = DefaultAuthenticatorCryptographyService(

View File

@ -2,7 +2,7 @@
/// An enumeration of session timeout values to choose from.
///
public enum SessionTimeoutValue: RawRepresentable, Equatable, Hashable, Sendable {
public enum SessionTimeoutValue: Codable, RawRepresentable, Equatable, Hashable, Sendable {
/// Time out immediately.
case immediately

View File

@ -1,4 +1,5 @@
import AuthenticatorBridgeKit
import BitwardenKit
import Foundation
// MARK: - KeychainService
@ -132,6 +133,6 @@ class DefaultKeychainService: KeychainService {
}
}
// MARK: - AuthenticatorKeychainService
// MARK: - SharedKeychainService
extension DefaultKeychainService: AuthenticatorKeychainService {}
extension DefaultKeychainService: SharedKeychainService {}

View File

@ -239,7 +239,7 @@ actor DefaultAuthenticatorSyncService: NSObject, AuthenticatorSyncService {
return
}
}
try sharedKeychainRepository.deleteAuthenticatorKey()
try await sharedKeychainRepository.deleteAuthenticatorKey()
}
/// Determine if the given userId has sync turned on and an unlocked vault. This method serves as the

View File

@ -1,4 +1,5 @@
import AuthenticatorBridgeKit
import AuthenticatorBridgeKitMocks
import BitwardenKitMocks
import BitwardenSdk
import Combine
@ -92,7 +93,7 @@ final class AuthenticatorSyncServiceTests: BitwardenTestCase { // swiftlint:disa
func test_createAuthenticatorKeyIfNeeded_keyAlreadyExists() async throws {
setupInitialState()
await subject.start()
let key = sharedKeychainRepository.generateKeyData()
let key = sharedKeychainRepository.generateMockKeyData()
try await sharedKeychainRepository.setAuthenticatorKey(key)
stateService.syncToAuthenticatorSubject.send(("1", true))

View File

@ -777,9 +777,13 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
storeType: .persisted
)
let sharedKeychainStorage = DefaultSharedKeychainStorage(
keychainService: keychainService,
sharedAppGroupIdentifier: Bundle.main.sharedAppGroupIdentifier
)
let sharedKeychainRepository = DefaultSharedKeychainRepository(
sharedAppGroupIdentifier: Bundle.main.sharedAppGroupIdentifier,
keychainService: keychainService
storage: sharedKeychainStorage
)
let sharedCryptographyService = DefaultAuthenticatorCryptographyService(

View File

@ -1,38 +0,0 @@
import CryptoKit
import Foundation
@testable import AuthenticatorBridgeKit
class MockSharedKeychainRepository {
var authenticatorKey: Data?
var errorToThrow: Error?
}
extension MockSharedKeychainRepository: SharedKeychainRepository {
func generateKeyData() -> Data {
let key = SymmetricKey(size: .bits256)
return key.withUnsafeBytes { Data(Array($0)) }
}
func deleteAuthenticatorKey() throws {
guard errorToThrow == nil else { throw errorToThrow! }
authenticatorKey = nil
}
func getAuthenticatorKey() async throws -> Data {
guard errorToThrow == nil else { throw errorToThrow! }
if let authenticatorKey {
return authenticatorKey
} else {
throw AuthenticatorKeychainServiceError.keyNotFound(.authenticatorKey)
}
}
func setAuthenticatorKey(_ value: Data) async throws {
guard errorToThrow == nil else { throw errorToThrow! }
authenticatorKey = value
}
}

View File

@ -79,7 +79,11 @@ targets:
sources:
- path: AuthenticatorBridgeKit
excludes:
- "**/Fixtures/*"
- "**/Mocks/*"
- "**/Tests/*"
- "**/*Tests.*"
- "**/TestHelpers/*"
dependencies:
- target: BitwardenKit
AuthenticatorBridgeKitTests:
@ -92,11 +96,31 @@ targets:
- path: AuthenticatorBridgeKit
includes:
- "**/Tests/*"
- "**/*Tests.*"
- "**/TestHelpers/*"
dependencies:
- target: AuthenticatorBridgeKit
- target: AuthenticatorBridgeKitMocks
- target: BitwardenKit
- target: BitwardenKitMocks
- target: TestHelpers
randomExecutionOrder: true
AuthenticatorBridgeKitMocks:
type: framework
platform: iOS
settings:
base:
ENABLE_TESTING_SEARCH_PATHS: YES
INFOPLIST_FILE: AuthenticatorBridgeKit/MocksInfo.plist
sources:
- path: AuthenticatorBridgeKit
includes:
- "**/Fixtures/*"
- "**/Mocks/*"
dependencies:
- target: AuthenticatorBridgeKit
- target: BitwardenKit
- target: TestHelpers
BitwardenKit:
type: framework
platform: iOS