Files
bookstax/BookStaxShareExtension/ShareExtensionKeychainService.swift
T

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)
}
}