mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-11 04:34:55 -06:00
[PM-21681] Build out Shared Keychain functionality (#1636)
This commit is contained in:
parent
e0aa268b98
commit
b609b053c2
22
AuthenticatorBridgeKit/MocksInfo.plist
Normal file
22
AuthenticatorBridgeKit/MocksInfo.plist
Normal 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>
|
||||||
@ -12,7 +12,7 @@ public protocol SharedCryptographyService: AnyObject {
|
|||||||
///
|
///
|
||||||
/// - Parameter items: The encrypted array of items to be decrypted
|
/// - Parameter items: The encrypted array of items to be decrypted
|
||||||
/// - Returns: the array of items with their data 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.
|
/// key is not in the shared repository.
|
||||||
///
|
///
|
||||||
func decryptAuthenticatorItems(
|
func decryptAuthenticatorItems(
|
||||||
@ -24,7 +24,7 @@ public protocol SharedCryptographyService: AnyObject {
|
|||||||
///
|
///
|
||||||
/// - Parameter items: The decrypted array of items to be encrypted
|
/// - Parameter items: The decrypted array of items to be encrypted
|
||||||
/// - Returns: the array of items with their data 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.
|
/// key is not in the shared repository.
|
||||||
///
|
///
|
||||||
func encryptAuthenticatorItems(
|
func encryptAuthenticatorItems(
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,11 +1,11 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
// MARK: - AuthenticatorKeychainService
|
// MARK: - SharedKeychainService
|
||||||
|
|
||||||
/// A Service to provide a wrapper around the device keychain shared via App Group between
|
/// A Service to provide a wrapper around the device keychain shared via App Group between
|
||||||
/// the Authenticator and the main Bitwarden app.
|
/// the Authenticator and the main Bitwarden app.
|
||||||
///
|
///
|
||||||
public protocol AuthenticatorKeychainService: AnyObject {
|
public protocol SharedKeychainService: AnyObject {
|
||||||
/// Adds a set of attributes.
|
/// Adds a set of attributes.
|
||||||
///
|
///
|
||||||
/// - Parameter attributes: Attributes to add.
|
/// - Parameter attributes: Attributes to add.
|
||||||
@ -26,10 +26,10 @@ public protocol AuthenticatorKeychainService: AnyObject {
|
|||||||
func search(query: CFDictionary) throws -> AnyObject?
|
func search(query: CFDictionary) throws -> AnyObject?
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - AuthenticatorKeychainServiceError
|
// MARK: - SharedKeychainServiceError
|
||||||
|
|
||||||
/// Enum with possible error cases that can be thrown from `AuthenticatorKeychainService`.
|
/// Enum with possible error cases that can be thrown from `SharedKeychainService`.
|
||||||
public enum AuthenticatorKeychainServiceError: Error, Equatable, CustomNSError {
|
public enum SharedKeychainServiceError: Error, Equatable, CustomNSError {
|
||||||
/// When a `KeychainService` is unable to locate an auth key for a given storage key.
|
/// 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.
|
/// - Parameter KeychainItem: The potential storage key for the auth key.
|
||||||
@ -4,82 +4,96 @@ import Foundation
|
|||||||
|
|
||||||
/// Enumeration of support Keychain Items that can be placed in the `SharedKeychainRepository`
|
/// 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.
|
/// The keychain item for the authenticator encryption key.
|
||||||
case authenticatorKey
|
case authenticatorKey
|
||||||
|
|
||||||
|
/// A date at which a PM account automatically logs out.
|
||||||
|
case accountAutoLogout(userId: String)
|
||||||
|
|
||||||
/// The storage key for this keychain item.
|
/// The storage key for this keychain item.
|
||||||
///
|
///
|
||||||
var unformattedKey: String {
|
public var unformattedKey: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .authenticatorKey:
|
case .authenticatorKey:
|
||||||
"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
|
||||||
/// A repository for managing keychain items to be shared between the main Bitwarden app and the Authenticator app.
|
/// talk with a `SharedKeychainService`.
|
||||||
///
|
///
|
||||||
public protocol SharedKeychainRepository: AnyObject {
|
public protocol SharedKeychainStorage {
|
||||||
/// Attempts to delete the authenticator key from the keychain.
|
/// Deletes the value in the keychain for the given item.
|
||||||
///
|
///
|
||||||
func deleteAuthenticatorKey() throws
|
/// - 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
|
||||||
|
|
||||||
/// Gets the authenticator key.
|
/// Retrieve the value for the specific item from the Keychain Service.
|
||||||
///
|
///
|
||||||
/// - Returns: Data representing the authenticator key.
|
/// - Parameter item: the keychain item for which to retrieve a value.
|
||||||
|
/// - Returns: The value (Data) stored in the keychain for the given item.
|
||||||
///
|
///
|
||||||
func getAuthenticatorKey() async throws -> Data
|
func getValue<T: Codable>(for item: SharedKeychainItem) async throws -> T
|
||||||
|
|
||||||
/// Stores the access token for a user in the keychain.
|
/// Store a given value into the keychain for the given item.
|
||||||
///
|
///
|
||||||
/// - Parameter value: The authenticator key to store.
|
/// - Parameters:
|
||||||
|
/// - value: The value (Data) to be stored into the keychain
|
||||||
|
/// - item: The item for which to store the value in the keychain.
|
||||||
///
|
///
|
||||||
func setAuthenticatorKey(_ value: Data) async throws
|
func setValue<T: Codable>(_ value: T, for item: SharedKeychainItem) async throws
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - DefaultKeychainRepository
|
public class DefaultSharedKeychainStorage: SharedKeychainStorage {
|
||||||
|
|
||||||
/// A concrete implementation of the `SharedKeychainRepository` protocol.
|
|
||||||
///
|
|
||||||
public class DefaultSharedKeychainRepository: SharedKeychainRepository {
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
|
/// The keychain service used by the repository
|
||||||
|
///
|
||||||
|
private let keychainService: SharedKeychainService
|
||||||
|
|
||||||
/// An identifier for the shared access group used by the application.
|
/// An identifier for the shared access group used by the application.
|
||||||
///
|
///
|
||||||
/// Example: "group.com.8bit.bitwarden"
|
/// Example: "group.com.8bit.bitwarden"
|
||||||
///
|
///
|
||||||
private let sharedAppGroupIdentifier: String
|
private let sharedAppGroupIdentifier: String
|
||||||
|
|
||||||
/// The keychain service used by the repository
|
|
||||||
///
|
|
||||||
private let keychainService: AuthenticatorKeychainService
|
|
||||||
|
|
||||||
// MARK: Initialization
|
// MARK: Initialization
|
||||||
|
|
||||||
/// Initialize a `DefaultSharedKeychainRepository`.
|
/// Initialize a `DefaultSharedKeychainStorage`.
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - sharedAppGroupIdentifier: An identifier for the shared access group used by the application.
|
|
||||||
/// - keychainService: The keychain service used by the repository
|
/// - keychainService: The keychain service used by the repository
|
||||||
|
/// - sharedAppGroupIdentifier: An identifier for the shared access group used by the application.
|
||||||
public init(
|
public init(
|
||||||
sharedAppGroupIdentifier: String,
|
keychainService: SharedKeychainService,
|
||||||
keychainService: AuthenticatorKeychainService
|
sharedAppGroupIdentifier: String
|
||||||
) {
|
) {
|
||||||
self.sharedAppGroupIdentifier = sharedAppGroupIdentifier
|
|
||||||
self.keychainService = keychainService
|
self.keychainService = keychainService
|
||||||
|
self.sharedAppGroupIdentifier = sharedAppGroupIdentifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Methods
|
// MARK: Methods
|
||||||
|
|
||||||
/// Retrieve the value for the specific item from the Keychain Service.
|
public func deleteValue(for item: SharedKeychainItem) async throws {
|
||||||
///
|
try keychainService.delete(
|
||||||
/// - Parameter item: the keychain item for which to retrieve a value.
|
query: [
|
||||||
/// - Returns: The value (Data) stored in the keychain for the given item.
|
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
||||||
///
|
kSecAttrAccessGroup: sharedAppGroupIdentifier,
|
||||||
private func getSharedValue(for item: SharedKeychainItem) async throws -> Data {
|
kSecAttrAccount: item.unformattedKey,
|
||||||
|
kSecClass: kSecClassGenericPassword,
|
||||||
|
] as CFDictionary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getValue<T: Codable>(for item: SharedKeychainItem) async throws -> T {
|
||||||
let foundItem = try keychainService.search(
|
let foundItem = try keychainService.search(
|
||||||
query: [
|
query: [
|
||||||
kSecMatchLimit: kSecMatchLimitOne,
|
kSecMatchLimit: kSecMatchLimitOne,
|
||||||
@ -93,20 +107,14 @@ public class DefaultSharedKeychainRepository: SharedKeychainRepository {
|
|||||||
)
|
)
|
||||||
|
|
||||||
guard let resultDictionary = foundItem as? [String: Any],
|
guard let resultDictionary = foundItem as? [String: Any],
|
||||||
let data = resultDictionary[kSecValueData as String] as? Data else {
|
let data = resultDictionary[kSecValueData as String] as? T else {
|
||||||
throw AuthenticatorKeychainServiceError.keyNotFound(item)
|
throw SharedKeychainServiceError.keyNotFound(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Store a given value into the keychain for the given item.
|
public func setValue<T: Codable>(_ value: T, for item: SharedKeychainItem) async throws {
|
||||||
///
|
|
||||||
/// - 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 = [
|
let query = [
|
||||||
kSecValueData: value,
|
kSecValueData: value,
|
||||||
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +1,23 @@
|
|||||||
|
import AuthenticatorBridgeKit
|
||||||
|
import AuthenticatorBridgeKitMocks
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import Foundation
|
import Foundation
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
@testable import AuthenticatorBridgeKit
|
final class SharedKeychainStorageTests: BitwardenTestCase {
|
||||||
|
|
||||||
final class SharedKeychainRepositoryTests: AuthenticatorBridgeKitTestCase {
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
let accessGroup = "group.com.example.bitwarden"
|
let accessGroup = "group.com.example.bitwarden"
|
||||||
var keychainService: MockAuthenticatorKeychainService!
|
var keychainService: MockSharedKeychainService!
|
||||||
var subject: DefaultSharedKeychainRepository!
|
var subject: SharedKeychainStorage!
|
||||||
|
|
||||||
// MARK: Setup & Teardown
|
// MARK: Setup & Teardown
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
keychainService = MockAuthenticatorKeychainService()
|
keychainService = MockSharedKeychainService()
|
||||||
subject = DefaultSharedKeychainRepository(
|
subject = DefaultSharedKeychainStorage(
|
||||||
sharedAppGroupIdentifier: accessGroup,
|
keychainService: keychainService,
|
||||||
keychainService: keychainService
|
sharedAppGroupIdentifier: accessGroup
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,10 +28,10 @@ final class SharedKeychainRepositoryTests: AuthenticatorBridgeKitTestCase {
|
|||||||
|
|
||||||
// MARK: Tests
|
// 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 {
|
func test_deleteValue_success() async throws {
|
||||||
try subject.deleteAuthenticatorKey()
|
try await subject.deleteValue(for: .authenticatorKey)
|
||||||
|
|
||||||
let queries = try XCTUnwrap(keychainService.deleteQueries as? [[CFString: Any]])
|
let queries = try XCTUnwrap(keychainService.deleteQueries as? [[CFString: Any]])
|
||||||
XCTAssertEqual(queries.count, 1)
|
XCTAssertEqual(queries.count, 1)
|
||||||
@ -46,16 +46,16 @@ final class SharedKeychainRepositoryTests: AuthenticatorBridgeKitTestCase {
|
|||||||
String(kSecClassGenericPassword))
|
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.
|
/// search attributes are specified correctly.
|
||||||
///
|
///
|
||||||
func test_getAuthenticatorKey_success() async throws {
|
func test_getValue_success() async throws {
|
||||||
let key = SymmetricKey(size: .bits256)
|
let key = SymmetricKey(size: .bits256)
|
||||||
let data = key.withUnsafeBytes { Data(Array($0)) }
|
let data = key.withUnsafeBytes { Data(Array($0)) }
|
||||||
|
|
||||||
keychainService.setSearchResultData(data)
|
keychainService.setSearchResultData(data)
|
||||||
|
|
||||||
let returnData = try await subject.getAuthenticatorKey()
|
let returnData: Data = try await subject.getValue(for: .authenticatorKey)
|
||||||
XCTAssertEqual(returnData, data)
|
XCTAssertEqual(returnData, data)
|
||||||
|
|
||||||
let query = try XCTUnwrap(keychainService.searchQuery as? [CFString: Any])
|
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
|
/// Verify that `getAuthenticatorKey()` fails with a `keyNotFound` error when an unexpected
|
||||||
/// result is returned instead of the key data from the keychain
|
/// result is returned instead of the key data from the keychain
|
||||||
///
|
///
|
||||||
func test_getAuthenticatorKey_badResult() async throws {
|
func test_getValue_badResult() async throws {
|
||||||
let error = AuthenticatorKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
|
let key = SharedKeychainItem.accountAutoLogout(userId: "1")
|
||||||
|
let error = SharedKeychainServiceError.keyNotFound(key)
|
||||||
keychainService.searchResult = .success([kSecValueData as String: NSObject()] as AnyObject)
|
keychainService.searchResult = .success([kSecValueData as String: NSObject()] as AnyObject)
|
||||||
|
|
||||||
await assertAsyncThrows(error: error) {
|
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
|
/// result is returned instead of the key data from the keychain
|
||||||
///
|
///
|
||||||
func test_getAuthenticatorKey_nilResult() async throws {
|
func test_getValue_nilResult() async throws {
|
||||||
let error = AuthenticatorKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
|
let key = SharedKeychainItem.accountAutoLogout(userId: "1")
|
||||||
|
let error = SharedKeychainServiceError.keyNotFound(key)
|
||||||
keychainService.searchResult = .success(nil)
|
keychainService.searchResult = .success(nil)
|
||||||
|
|
||||||
await assertAsyncThrows(error: error) {
|
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
|
/// present in the keychain
|
||||||
///
|
///
|
||||||
func test_getAuthenticatorKey_keyNotFound() async throws {
|
func test_getAuthenticatorKey_keyNotFound() async throws {
|
||||||
let error = AuthenticatorKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
|
let error = SharedKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
|
||||||
keychainService.searchResult = .failure(error)
|
keychainService.searchResult = .failure(error)
|
||||||
|
|
||||||
await assertAsyncThrows(error: 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 {
|
func test_setAuthenticatorKey_success() async throws {
|
||||||
let key = SymmetricKey(size: .bits256)
|
let key = SymmetricKey(size: .bits256)
|
||||||
let data = key.withUnsafeBytes { Data(Array($0)) }
|
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])
|
let attributes = try XCTUnwrap(keychainService.addAttributes as? [CFString: Any])
|
||||||
try XCTAssertEqual(XCTUnwrap(attributes[kSecAttrAccessGroup] as? String), accessGroup)
|
try XCTAssertEqual(XCTUnwrap(attributes[kSecAttrAccessGroup] as? String), accessGroup)
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import AuthenticatorBridgeKit
|
||||||
|
import AuthenticatorBridgeKitMocks
|
||||||
import BitwardenKit
|
import BitwardenKit
|
||||||
import BitwardenKitMocks
|
import BitwardenKitMocks
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import AuthenticatorBridgeKit
|
||||||
|
import AuthenticatorBridgeKitMocks
|
||||||
import BitwardenKit
|
import BitwardenKit
|
||||||
import BitwardenKitMocks
|
import BitwardenKitMocks
|
||||||
import Foundation
|
import Foundation
|
||||||
@ -170,7 +172,7 @@ final class AuthenticatorBridgeItemServiceTests: AuthenticatorBridgeKitTestCase
|
|||||||
/// Verify that `isSyncOn` returns true when the key is present in the keychain.
|
/// Verify that `isSyncOn` returns true when the key is present in the keychain.
|
||||||
///
|
///
|
||||||
func test_isSyncOn_true() async throws {
|
func test_isSyncOn_true() async throws {
|
||||||
let key = keychainRepository.generateKeyData()
|
let key = keychainRepository.generateMockKeyData()
|
||||||
try await keychainRepository.setAuthenticatorKey(key)
|
try await keychainRepository.setAuthenticatorKey(key)
|
||||||
let sync = await subject.isSyncOn()
|
let sync = await subject.isSyncOn()
|
||||||
XCTAssertTrue(sync)
|
XCTAssertTrue(sync)
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import AuthenticatorBridgeKit
|
||||||
|
import AuthenticatorBridgeKitMocks
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import Foundation
|
import Foundation
|
||||||
import XCTest
|
import XCTest
|
||||||
@ -16,7 +18,7 @@ final class SharedCryptographyServiceTests: AuthenticatorBridgeKitTestCase {
|
|||||||
override func setUp() {
|
override func setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
sharedKeychainRepository = MockSharedKeychainRepository()
|
sharedKeychainRepository = MockSharedKeychainRepository()
|
||||||
sharedKeychainRepository.authenticatorKey = sharedKeychainRepository.generateKeyData()
|
sharedKeychainRepository.authenticatorKey = sharedKeychainRepository.generateMockKeyData()
|
||||||
subject = DefaultAuthenticatorCryptographyService(
|
subject = DefaultAuthenticatorCryptographyService(
|
||||||
sharedKeychainRepository: sharedKeychainRepository
|
sharedKeychainRepository: sharedKeychainRepository
|
||||||
)
|
)
|
||||||
@ -56,7 +58,7 @@ final class SharedCryptographyServiceTests: AuthenticatorBridgeKitTestCase {
|
|||||||
///
|
///
|
||||||
func test_decryptAuthenticatorItems_throwsKeyMissingError() async throws {
|
func test_decryptAuthenticatorItems_throwsKeyMissingError() async throws {
|
||||||
let encryptedItems = try await subject.encryptAuthenticatorItems(items)
|
let encryptedItems = try await subject.encryptAuthenticatorItems(items)
|
||||||
let error = AuthenticatorKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
|
let error = SharedKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
|
||||||
|
|
||||||
try sharedKeychainRepository.deleteAuthenticatorKey()
|
try sharedKeychainRepository.deleteAuthenticatorKey()
|
||||||
await assertAsyncThrows(error: error) {
|
await assertAsyncThrows(error: error) {
|
||||||
@ -110,7 +112,7 @@ final class SharedCryptographyServiceTests: AuthenticatorBridgeKitTestCase {
|
|||||||
/// when the `SharedKeyRepository` authenticator key is missing.
|
/// when the `SharedKeyRepository` authenticator key is missing.
|
||||||
///
|
///
|
||||||
func test_encryptAuthenticatorItems_throwsKeyMissingError() async throws {
|
func test_encryptAuthenticatorItems_throwsKeyMissingError() async throws {
|
||||||
let error = AuthenticatorKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
|
let error = SharedKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
|
||||||
|
|
||||||
try sharedKeychainRepository.deleteAuthenticatorKey()
|
try sharedKeychainRepository.deleteAuthenticatorKey()
|
||||||
await assertAsyncThrows(error: error) {
|
await assertAsyncThrows(error: error) {
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import TestHelpers
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
open class BitwardenTestCase: BaseBitwardenTestCase {
|
||||||
|
@MainActor
|
||||||
|
override open class func setUp() {
|
||||||
|
TestDataHelpers.defaultBundle = Bundle(for: Self.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import AuthenticatorBridgeKit
|
import AuthenticatorBridgeKit
|
||||||
|
import BitwardenKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
// MARK: - KeychainService
|
// MARK: - KeychainService
|
||||||
@ -112,6 +113,6 @@ class DefaultKeychainService: KeychainService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - AuthenticatorKeychainService
|
// MARK: - SharedKeychainService
|
||||||
|
|
||||||
extension DefaultKeychainService: AuthenticatorKeychainService {}
|
extension DefaultKeychainService: SharedKeychainService {}
|
||||||
|
|||||||
@ -226,9 +226,13 @@ public class ServiceContainer: Services {
|
|||||||
authenticatorItemDataStore: dataStore
|
authenticatorItemDataStore: dataStore
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let sharedKeychainStorage = DefaultSharedKeychainStorage(
|
||||||
|
keychainService: keychainService,
|
||||||
|
sharedAppGroupIdentifier: Bundle.main.sharedAppGroupIdentifier
|
||||||
|
)
|
||||||
|
|
||||||
let sharedKeychainRepository = DefaultSharedKeychainRepository(
|
let sharedKeychainRepository = DefaultSharedKeychainRepository(
|
||||||
sharedAppGroupIdentifier: Bundle.main.sharedAppGroupIdentifier,
|
storage: sharedKeychainStorage
|
||||||
keychainService: keychainService
|
|
||||||
)
|
)
|
||||||
|
|
||||||
let sharedCryptographyService = DefaultAuthenticatorCryptographyService(
|
let sharedCryptographyService = DefaultAuthenticatorCryptographyService(
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
/// An enumeration of session timeout values to choose from.
|
/// 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.
|
/// Time out immediately.
|
||||||
case immediately
|
case immediately
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import AuthenticatorBridgeKit
|
import AuthenticatorBridgeKit
|
||||||
|
import BitwardenKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
// MARK: - KeychainService
|
// MARK: - KeychainService
|
||||||
@ -132,6 +133,6 @@ class DefaultKeychainService: KeychainService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - AuthenticatorKeychainService
|
// MARK: - SharedKeychainService
|
||||||
|
|
||||||
extension DefaultKeychainService: AuthenticatorKeychainService {}
|
extension DefaultKeychainService: SharedKeychainService {}
|
||||||
|
|||||||
@ -239,7 +239,7 @@ actor DefaultAuthenticatorSyncService: NSObject, AuthenticatorSyncService {
|
|||||||
return
|
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
|
/// Determine if the given userId has sync turned on and an unlocked vault. This method serves as the
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import AuthenticatorBridgeKit
|
import AuthenticatorBridgeKit
|
||||||
|
import AuthenticatorBridgeKitMocks
|
||||||
import BitwardenKitMocks
|
import BitwardenKitMocks
|
||||||
import BitwardenSdk
|
import BitwardenSdk
|
||||||
import Combine
|
import Combine
|
||||||
@ -92,7 +93,7 @@ final class AuthenticatorSyncServiceTests: BitwardenTestCase { // swiftlint:disa
|
|||||||
func test_createAuthenticatorKeyIfNeeded_keyAlreadyExists() async throws {
|
func test_createAuthenticatorKeyIfNeeded_keyAlreadyExists() async throws {
|
||||||
setupInitialState()
|
setupInitialState()
|
||||||
await subject.start()
|
await subject.start()
|
||||||
let key = sharedKeychainRepository.generateKeyData()
|
let key = sharedKeychainRepository.generateMockKeyData()
|
||||||
try await sharedKeychainRepository.setAuthenticatorKey(key)
|
try await sharedKeychainRepository.setAuthenticatorKey(key)
|
||||||
|
|
||||||
stateService.syncToAuthenticatorSubject.send(("1", true))
|
stateService.syncToAuthenticatorSubject.send(("1", true))
|
||||||
|
|||||||
@ -777,9 +777,13 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
|
|||||||
storeType: .persisted
|
storeType: .persisted
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let sharedKeychainStorage = DefaultSharedKeychainStorage(
|
||||||
|
keychainService: keychainService,
|
||||||
|
sharedAppGroupIdentifier: Bundle.main.sharedAppGroupIdentifier
|
||||||
|
)
|
||||||
|
|
||||||
let sharedKeychainRepository = DefaultSharedKeychainRepository(
|
let sharedKeychainRepository = DefaultSharedKeychainRepository(
|
||||||
sharedAppGroupIdentifier: Bundle.main.sharedAppGroupIdentifier,
|
storage: sharedKeychainStorage
|
||||||
keychainService: keychainService
|
|
||||||
)
|
)
|
||||||
|
|
||||||
let sharedCryptographyService = DefaultAuthenticatorCryptographyService(
|
let sharedCryptographyService = DefaultAuthenticatorCryptographyService(
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -79,7 +79,11 @@ targets:
|
|||||||
sources:
|
sources:
|
||||||
- path: AuthenticatorBridgeKit
|
- path: AuthenticatorBridgeKit
|
||||||
excludes:
|
excludes:
|
||||||
|
- "**/Fixtures/*"
|
||||||
|
- "**/Mocks/*"
|
||||||
- "**/Tests/*"
|
- "**/Tests/*"
|
||||||
|
- "**/*Tests.*"
|
||||||
|
- "**/TestHelpers/*"
|
||||||
dependencies:
|
dependencies:
|
||||||
- target: BitwardenKit
|
- target: BitwardenKit
|
||||||
AuthenticatorBridgeKitTests:
|
AuthenticatorBridgeKitTests:
|
||||||
@ -92,11 +96,31 @@ targets:
|
|||||||
- path: AuthenticatorBridgeKit
|
- path: AuthenticatorBridgeKit
|
||||||
includes:
|
includes:
|
||||||
- "**/Tests/*"
|
- "**/Tests/*"
|
||||||
|
- "**/*Tests.*"
|
||||||
|
- "**/TestHelpers/*"
|
||||||
dependencies:
|
dependencies:
|
||||||
- target: AuthenticatorBridgeKit
|
- target: AuthenticatorBridgeKit
|
||||||
|
- target: AuthenticatorBridgeKitMocks
|
||||||
|
- target: BitwardenKit
|
||||||
- target: BitwardenKitMocks
|
- target: BitwardenKitMocks
|
||||||
- target: TestHelpers
|
- target: TestHelpers
|
||||||
randomExecutionOrder: true
|
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:
|
BitwardenKit:
|
||||||
type: framework
|
type: framework
|
||||||
platform: iOS
|
platform: iOS
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user