87 lines
3.3 KiB
Swift
87 lines
3.3 KiB
Swift
import Foundation
|
|
import Security
|
|
|
|
/// Shared Keychain service for passing credentials between the main app
|
|
/// and the Share Extension via App Group "group.de.hanold.bookstax".
|
|
///
|
|
/// - The main app calls `saveCredentials` whenever a profile is activated.
|
|
/// - The extension calls `loadCredentials` to authenticate API requests.
|
|
/// - Accessibility is `afterFirstUnlock` so the extension can run while the
|
|
/// device is locked after the user has unlocked it at least once.
|
|
///
|
|
/// Add this file to **both** the main app target and `BookStaxShareExtension`.
|
|
enum ShareExtensionKeychainService {
|
|
|
|
private static let service = "de.hanold.bookstax.shared"
|
|
private static let account = "activeCredentials"
|
|
private static let accessGroup = "group.de.hanold.bookstax"
|
|
|
|
private struct Credentials: Codable {
|
|
let serverURL: String
|
|
let tokenId: String
|
|
let tokenSecret: String
|
|
}
|
|
|
|
// MARK: - Save (called from main app)
|
|
|
|
/// Persists the active profile credentials in the shared keychain.
|
|
static func saveCredentials(serverURL: String, tokenId: String, tokenSecret: String) {
|
|
guard let data = try? JSONEncoder().encode(
|
|
Credentials(serverURL: serverURL, tokenId: tokenId, tokenSecret: tokenSecret)
|
|
) else { return }
|
|
|
|
let baseQuery: [CFString: Any] = [
|
|
kSecClass: kSecClassGenericPassword,
|
|
kSecAttrService: service,
|
|
kSecAttrAccount: account,
|
|
kSecAttrAccessGroup: accessGroup
|
|
]
|
|
|
|
// Try update first; add if not found.
|
|
let updateStatus = SecItemUpdate(
|
|
baseQuery as CFDictionary,
|
|
[kSecValueData: data] as CFDictionary
|
|
)
|
|
if updateStatus == errSecItemNotFound {
|
|
var addQuery = baseQuery
|
|
addQuery[kSecValueData] = data
|
|
addQuery[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlock
|
|
SecItemAdd(addQuery as CFDictionary, nil)
|
|
}
|
|
}
|
|
|
|
// MARK: - Load (called from Share Extension)
|
|
|
|
/// Returns the stored credentials, or `nil` if the user has not yet
|
|
/// configured the main app.
|
|
static func loadCredentials() -> (serverURL: String, tokenId: String, tokenSecret: String)? {
|
|
let query: [CFString: Any] = [
|
|
kSecClass: kSecClassGenericPassword,
|
|
kSecAttrService: service,
|
|
kSecAttrAccount: account,
|
|
kSecAttrAccessGroup: accessGroup,
|
|
kSecReturnData: true,
|
|
kSecMatchLimit: kSecMatchLimitOne
|
|
]
|
|
var result: AnyObject?
|
|
guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
|
|
let data = result as? Data,
|
|
let creds = try? JSONDecoder().decode(Credentials.self, from: data)
|
|
else { return nil }
|
|
return (creds.serverURL, creds.tokenId, creds.tokenSecret)
|
|
}
|
|
|
|
// MARK: - Clear
|
|
|
|
/// Removes the shared credentials (e.g., on logout).
|
|
static func clearCredentials() {
|
|
let query: [CFString: Any] = [
|
|
kSecClass: kSecClassGenericPassword,
|
|
kSecAttrService: service,
|
|
kSecAttrAccount: account,
|
|
kSecAttrAccessGroup: accessGroup
|
|
]
|
|
SecItemDelete(query as CFDictionary)
|
|
}
|
|
}
|