mana-swift-core/Sources/ManaCore/Auth/KeychainStore.swift
Till JS df6f67ee45 v1.0.0 — initiale Extraktion aus memoro-native
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>
2026-05-12 19:13:31 +02:00

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