Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Encrypt items #24

Merged
merged 4 commits into from
Apr 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions AuthenticatorShared/Core/Auth/Services/AppIdService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation

/// A service that manages getting and creating the app's ID.
///
actor AppIdService {
// MARK: Properties

/// The app settings store used to persist app values.
let appSettingStore: AppSettingsStore

// MARK: Initialization

/// Initialize an `AppIdService`.
///
/// - Parameter appSettingStore: The app settings store used to persist app values.
///
init(appSettingStore: AppSettingsStore) {
self.appSettingStore = appSettingStore
}

// MARK: Methods

/// Returns the app's ID if it exists or creates a new one.
///
/// - Returns: The app's ID.
///
func getOrCreateAppId() -> String {
if let appId = appSettingStore.appId {
return appId
} else {
let appId = UUID().uuidString
appSettingStore.appId = appId
return appId
}
}
}
201 changes: 201 additions & 0 deletions AuthenticatorShared/Core/Auth/Services/KeychainRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import Foundation

// MARK: - KeychainItem

enum KeychainItem: Equatable {
/// The keychain item for a user's encryption secret key.
case secretKey(userId: String)

/// The `SecAccessControlCreateFlags` protection level for this keychain item.
/// If `nil`, no extra protection is applied.
///
var protection: SecAccessControlCreateFlags? {
switch self {
case .secretKey:
nil
}
}

/// The storage key for this keychain item.
///
var unformattedKey: String {
switch self {
case let .secretKey(userId):
"secretKey_\(userId)"
}
}
}

// MARK: - KeychainRepository

protocol KeychainRepository: AnyObject {
/// Gets the stored secret key for a user from the keychain.
///
/// - Parameters:
/// - userId: The user ID associated with the stored secret key.
/// - Returns: The user's secret key.
///
func getSecretKey(userId: String) async throws -> String

/// Stores the secret key for a user in the keychain
///
/// - Parameters:
/// - value: The secret key to store.
/// - userId: The user's ID
///
func setSecretKey(_ value: String, userId: String) async throws
}

extension KeychainRepository {
/// The format for storing the `unformattedKey` of a `KeychainItem`.
/// The first value should be a unique appID from the `appIdService`.
/// The second value is the `unformattedKey`
///
/// Example: `bwKeyChainStorage:1234567890:biometric_key_98765`
///
var storageKeyFormat: String { "bwaKeychainStorage:%@:%@" }
}

// MARK: - DefaultKeychainRepository

class DefaultKeychainRepository: KeychainRepository {
// MARK: Properties

/// A service used to provide unique app ids.
///
let appIdService: AppIdService

/// An identifier for this application and extensions.
/// ie: "LTZ2PFU5D6.com.8bit.bitwarden"
///
var appSecAttrService: String {
Bundle.main.appIdentifier
}

/// An identifier for this application group and extensions
/// ie: "group.LTZ2PFU5D6.com.8bit.bitwarden"
///
var appSecAttrAccessGroup: String {
Bundle.main.groupIdentifier
}

/// The keychain service used by the repository
///
let keychainService: KeychainService

// MARK: Initialization

init(
appIdService: AppIdService,
keychainService: KeychainService
) {
self.appIdService = appIdService
self.keychainService = keychainService
}

// MARK: Methods

/// Generates a formatted storage key for a keychain item.
///
/// - Parameter item: The keychain item that needs a formatted key.
/// - Returns: A formatted storage key.
///
func formattedKey(for item: KeychainItem) async -> String {
let appId = await appIdService.getOrCreateAppId()
return String(format: storageKeyFormat, appId, item.unformattedKey)
}

/// Gets the value associated with the keychain item from the keychain.
///
/// - Parameter item: The keychain item used to fetch the associated value.
/// - Returns: The fetched value associated with the keychain item.
///
func getValue(for item: KeychainItem) async throws -> String {
let foundItem = try await keychainService.search(
query: keychainQueryValues(
for: item,
adding: [
kSecMatchLimit: kSecMatchLimitOne,
kSecReturnData: true,
kSecReturnAttributes: true,
]
)
)

if let resultDictionary = foundItem as? [String: Any],
let data = resultDictionary[kSecValueData as String] as? Data {
let string = String(decoding: data, as: UTF8.self)
guard !string.isEmpty else {
throw KeychainServiceError.keyNotFound(item)
}
return string
}

throw KeychainServiceError.keyNotFound(item)
}

/// The core key/value pairs for Keychain operations
///
/// - Parameter item: The `KeychainItem` to be queried.
///
func keychainQueryValues(
for item: KeychainItem,
adding additionalPairs: [CFString: Any] = [:]
) async -> CFDictionary {
// Prepare a formatted `kSecAttrAccount` value.
let formattedSecAttrAccount = await formattedKey(for: item)

// Configure the base dictionary
var result: [CFString: Any] = [
kSecAttrAccount: formattedSecAttrAccount,
kSecAttrAccessGroup: appSecAttrAccessGroup,
kSecAttrService: appSecAttrService,
kSecClass: kSecClassGenericPassword,
]

// Add the additional key value pairs.
additionalPairs.forEach { key, value in
result[key] = value
}

return result as CFDictionary
}

/// Sets a value associated with a keychain item in the keychain.
///
/// - Parameters:
/// - value: The value associated with the keychain item to set.
/// - item: The keychain item used to set the associated value.
///
func setValue(_ value: String, for item: KeychainItem) async throws {
let accessControl = try keychainService.accessControl(
for: item.protection ?? []
)
let query = await keychainQueryValues(
for: item,
adding: [
kSecAttrAccessControl: accessControl as Any,
kSecValueData: Data(value.utf8),
]
)

// Delete the previous secret, if it exists,
// otherwise we get `errSecDuplicateItem`.
try? keychainService.delete(query: query)

// Add the new key.
try keychainService.add(
attributes: query
)
}
}

extension DefaultKeychainRepository {
func getSecretKey(userId: String) async throws -> String {
try await getValue(for: .secretKey(userId: userId))
}

func setSecretKey(_ value: String, userId: String) async throws {
try await setValue(value, for: .secretKey(userId: userId))
}
}
112 changes: 112 additions & 0 deletions AuthenticatorShared/Core/Auth/Services/KeychainService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import Foundation

// MARK: - KeychainService

/// A Service to provide a wrapper around the device Keychain.
///
protocol KeychainService: AnyObject {
/// Creates an access control for a given set of flags.
///
/// - Parameter flags: The `SecAccessControlCreateFlags` for the access control.
/// - Returns: The SecAccessControl.
///
func accessControl(
for flags: SecAccessControlCreateFlags
) throws -> SecAccessControl

/// 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 delete.
/// - Returns: The search results.
///
func search(query: CFDictionary) throws -> AnyObject?
}

// MARK: - KeychainServiceError

enum KeychainServiceError: Error, Equatable {
/// When creating an accessControl fails.
///
/// - Parameter CFError: The potential system error.
///
case accessControlFailed(CFError?)

/// 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(KeychainItem)

/// A passthrough for OSService Error cases.
///
/// - Parameter OSStatus: The `OSStatus` returned from a keychain operation.
///
case osStatusError(OSStatus)
}

// MARK: - DefaultKeychainService

class DefaultKeychainService: KeychainService {
// MARK: Methods

func accessControl(
for flags: SecAccessControlCreateFlags
) throws -> SecAccessControl {
var error: Unmanaged<CFError>?
let accessControl = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
flags,
&error
)

guard let accessControl,
error == nil
else {
throw KeychainServiceError.accessControlFailed(error?.takeUnretainedValue())
}
return accessControl
}

func add(attributes: CFDictionary) throws {
try resolve(SecItemAdd(attributes, nil))
}

func delete(query: CFDictionary) throws {
try resolve(SecItemDelete(query))
}

func search(query: CFDictionary) throws -> AnyObject? {
var foundItem: AnyObject?
try resolve(SecItemCopyMatching(query, &foundItem))
return foundItem
}

// MARK: Private Methods

/// Ensures that a given status is a success.
/// Throws if not `errSecSuccess`.
///
/// - Parameter status: The OSStatus to check.
///
private func resolve(_ status: OSStatus) throws {
switch status {
case errSecSuccess:
break
default:
throw KeychainServiceError.osStatusError(status)
}
}
}
Loading