fix(keychain): Migration-Fallback bei accessGroup-Wechsel (v1.5.1)
Bis jetzt haben die mana-Apps `keychainAccessGroup: nil` gesetzt; Apple legt das Item dann im default-bucket (`$(AppIdentifierPrefix). $(BundleId)`) ab. Beim Wechsel auf eine explizite `accessGroup` (Apps werden mit v1.5.1 nachgezogen) hätten User sonst beim ersten Start einen Logout gesehen — Apple's Read-mit-Group liefert das alte Item nicht immer. KeychainStore.getString(for:) liest jetzt bei einem Miss einmalig ohne `kSecAttrAccessGroup` nach. Findet sich der alte Eintrag im default-bucket, wird er in den expliziten Bucket migriert und der alte gelöscht. Transparent für alle Caller. Greift nur wenn accessGroup != nil — Apps die nicht migrieren, sind unaffected. 70/70 ManaCore-Tests grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
957a80251e
commit
a70f7fa5e8
2 changed files with 57 additions and 2 deletions
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -4,6 +4,31 @@ Alle Änderungen werden hier dokumentiert. Format orientiert an
|
|||
[Keep a Changelog](https://keepachangelog.com), Versionierung nach
|
||||
[Semver](https://semver.org).
|
||||
|
||||
## [1.5.1] — 2026-05-17
|
||||
|
||||
Patch — KeychainStore-Migration-Fallback. Bisher haben die mana-Apps
|
||||
`keychainAccessGroup: nil` gesetzt; Apple legt das Item dann im
|
||||
default-bucket (`$(AppIdentifierPrefix).$(BundleId)`) ab. Beim
|
||||
Wechsel auf eine explizite `accessGroup` (Apps werden mit v1.5.1
|
||||
nachgezogen) hätten User sonst beim ersten Start einen Logout
|
||||
gesehen, weil Apples Read-mit-Group das alte Item nicht immer
|
||||
liefert.
|
||||
|
||||
### Verändert
|
||||
|
||||
- `KeychainStore.getString(for:)` — bei `accessGroup != nil` und
|
||||
einem Miss wird einmalig ohne `kSecAttrAccessGroup` re-queryt.
|
||||
Findet sich der alte Eintrag im default-bucket, wird er in den
|
||||
expliziten Bucket migriert und der alte gelöscht. Transparent für
|
||||
alle Caller — `getString` liefert wie gewohnt den Wert zurück.
|
||||
|
||||
### Migrations-Hinweis
|
||||
|
||||
- Apps, die `keychainAccessGroup` ab v1.5.1 explizit setzen, brauchen
|
||||
keinen Logout zu erwarten. Apps, die weiterhin `nil` setzen, sind
|
||||
von dem Fallback nicht betroffen (er greift nur bei `accessGroup
|
||||
!= nil`).
|
||||
|
||||
## [1.5.0] — 2026-05-14
|
||||
|
||||
Minor — `getProfile()` + `ProfileInfo`. Apps können den 2FA-Status
|
||||
|
|
|
|||
|
|
@ -61,10 +61,40 @@ public final class KeychainStore: Sendable {
|
|||
query[kSecMatchLimit] = kSecMatchLimitOne
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
guard status == errSecSuccess, let data = result as? Data else {
|
||||
if status == errSecSuccess, let data = result as? Data {
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
// One-shot migration: bis v1.2.0 haben die mana-Apps Keychain-
|
||||
// Items ohne explizite `kSecAttrAccessGroup` geschrieben. Apple
|
||||
// hat sie dann im default-bucket (= `$(AppIdentifierPrefix).$(BundleId)`)
|
||||
// gelandet. Ab v1.2.1 setzen die Apps die accessGroup explizit
|
||||
// auf denselben Identifier. In den meisten Fällen liefert Apple
|
||||
// das alte Item beim Read-mit-Group transparent zurück — wenn
|
||||
// nicht, hat das einen Logout zur Folge. Dieser Fallback liest
|
||||
// den Item nochmal ohne accessGroup, schreibt ihn beim Erfolg
|
||||
// mit accessGroup neu in den expliziten Bucket und löscht den
|
||||
// alten Eintrag.
|
||||
guard accessGroup != nil else { return nil }
|
||||
var fallback: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrService: service,
|
||||
kSecAttrAccount: key.rawValue,
|
||||
kSecReturnData: true,
|
||||
kSecMatchLimit: kSecMatchLimitOne,
|
||||
]
|
||||
var fallbackResult: AnyObject?
|
||||
guard SecItemCopyMatching(fallback as CFDictionary, &fallbackResult) == errSecSuccess,
|
||||
let data = fallbackResult as? Data,
|
||||
let value = String(data: data, encoding: .utf8)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return String(data: data, encoding: .utf8)
|
||||
try? setString(value, for: key)
|
||||
fallback.removeValue(forKey: kSecReturnData)
|
||||
fallback.removeValue(forKey: kSecMatchLimit)
|
||||
SecItemDelete(fallback as CFDictionary)
|
||||
return value
|
||||
}
|
||||
|
||||
public func remove(for key: Key) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue