ManaCore + ManaTokens als Swift-Package für alle nativen mana-e.V.-Apps. Phase α aus mana/docs/MANA_SWIFT.md durch. ManaCore: - AuthClient gegen mana-auth (Login, Refresh, Status-Maschine) - AuthenticatedTransport (URLSession + 401-Retry) - ManaAppConfig-Protocol für App-injizierbare Konfig - KeychainStore mit optionaler Shared-Access-Group - JWT-Parser für lokale Expiry-Prüfung - AuthError, CoreLog (interne OSLog-Logger) ManaTokens: - 12 Vereins-Tokens als dynamic Light/Dark Colors - 5 Brand-Literale (mana-yellow, spectrum-orange, ...) - Spacing, Radius, Typography aus mana/docs/THEMING.md Tests: 12 Unit-Tests grün via swift test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
84 lines
2.5 KiB
Swift
84 lines
2.5 KiB
Swift
import Foundation
|
|
import Security
|
|
|
|
/// Generische, in der App injizierbare Keychain-Wrapper. Eine Instanz
|
|
/// pro `ManaAppConfig`.
|
|
public final class KeychainStore: Sendable {
|
|
/// Bekannte Keys für mana-auth-Tokens.
|
|
public enum Key: String, Sendable {
|
|
case accessToken = "access_token"
|
|
case refreshToken = "refresh_token"
|
|
case email
|
|
}
|
|
|
|
private let service: String
|
|
private let accessGroup: String?
|
|
|
|
public init(service: String, accessGroup: String? = nil) {
|
|
self.service = service
|
|
self.accessGroup = accessGroup
|
|
}
|
|
|
|
public func setString(_ value: String, for key: Key) throws {
|
|
guard let data = value.data(using: .utf8) else {
|
|
throw AuthError.encoding
|
|
}
|
|
|
|
var query = baseQuery(for: key)
|
|
let attributes: [CFString: Any] = [
|
|
kSecValueData: data,
|
|
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock,
|
|
]
|
|
|
|
let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
|
|
switch updateStatus {
|
|
case errSecSuccess:
|
|
return
|
|
case errSecItemNotFound:
|
|
for (attrKey, attrValue) in attributes {
|
|
query[attrKey] = attrValue
|
|
}
|
|
let addStatus = SecItemAdd(query as CFDictionary, nil)
|
|
guard addStatus == errSecSuccess else {
|
|
throw AuthError.keychain(addStatus)
|
|
}
|
|
default:
|
|
throw AuthError.keychain(updateStatus)
|
|
}
|
|
}
|
|
|
|
public func getString(for key: Key) -> String? {
|
|
var query = baseQuery(for: key)
|
|
query[kSecReturnData] = true
|
|
query[kSecMatchLimit] = kSecMatchLimitOne
|
|
var result: AnyObject?
|
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
guard status == errSecSuccess, let data = result as? Data else {
|
|
return nil
|
|
}
|
|
return String(data: data, encoding: .utf8)
|
|
}
|
|
|
|
public func remove(for key: Key) {
|
|
let query = baseQuery(for: key)
|
|
SecItemDelete(query as CFDictionary)
|
|
}
|
|
|
|
public func wipe() {
|
|
remove(for: .accessToken)
|
|
remove(for: .refreshToken)
|
|
remove(for: .email)
|
|
}
|
|
|
|
private func baseQuery(for key: Key) -> [CFString: Any] {
|
|
var query: [CFString: Any] = [
|
|
kSecClass: kSecClassGenericPassword,
|
|
kSecAttrService: service,
|
|
kSecAttrAccount: key.rawValue,
|
|
]
|
|
if let accessGroup {
|
|
query[kSecAttrAccessGroup] = accessGroup
|
|
}
|
|
return query
|
|
}
|
|
}
|