mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 13:12:53 +00:00
The pre-revamp release reads its unscoped Keychain item with kSecAttrSynchronizableAny and no identity in the key, so a credential retained across an Apple Account change is already served to whoever holds the device. Asking before reusing it prevents nothing that is not already happening, and it cost every upgrading user a trip into the instance editor to find a button they had no reason to look for. What silent adoption could genuinely add is re-filing that secret as synchronizable under the identity signed in now, which uploads a login of unproven provenance into that person's iCloud Keychain and spreads it to their devices. So the copy is written device-local instead, and the migration runs where the status is computed. The real owner loses nothing: the item it was copied from syncs, so each of their devices finds it and adopts its own copy, and adoption still never deletes it. Two things keep that closed. Turning iCloud sync on rewrites stored credentials to follow the setting, which would undo the whole point, so adopted ones are left out of it. And only the account the instance's legacy login became may inherit the secret — the old item is keyed by instance, so a second login the user added themselves would otherwise match it.
87 lines
4.0 KiB
Swift
87 lines
4.0 KiB
Swift
//
|
|
// AccountCredentialsManager.swift
|
|
// Yattee
|
|
//
|
|
// Account-keyed credential storage — the multi-login replacement for the
|
|
// instance-keyed InstanceCredentialsManager.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// Credential storage keyed by `Account`, not by `Instance`.
|
|
///
|
|
/// Adopted by `InvidiousCredentialsManager` and `PipedCredentialsManager`.
|
|
/// `BasicAuthCredentialsManager` deliberately does **not** adopt it and keeps
|
|
/// `InstanceCredentialsManager`: HTTP Basic auth is a property of the instance
|
|
/// (or the reverse proxy in front of it), not of a user account, and a single
|
|
/// proxy credential covers every account on that instance.
|
|
@MainActor
|
|
protocol AccountCredentialsManager: AnyObject, Observable {
|
|
/// Accounts with a stored credential under the **current** identity scope.
|
|
var loggedInAccountIDs: Set<UUID> { get }
|
|
|
|
/// Whether the identity scope has been resolved yet.
|
|
///
|
|
/// While `false`, every read below reports "no credential" because the app
|
|
/// cannot yet tell which scope to look in. Callers must render that as
|
|
/// *temporarily unavailable*, never as "signed out" — correcting state on
|
|
/// it would discard a perfectly good login.
|
|
var isScopeResolved: Bool { get }
|
|
|
|
func setCredential(_ credential: String, for account: Account)
|
|
func credential(for account: Account) -> String?
|
|
func deleteCredential(for account: Account)
|
|
|
|
/// Deletes the credential and stored login identifier for an account this
|
|
/// device may know nothing else about.
|
|
///
|
|
/// A deletion can reach this device before the account it deletes does —
|
|
/// the tombstone and the account are separate records, and iCloud Keychain
|
|
/// runs on its own timeline. The secret must go regardless, and the scoped
|
|
/// key needs only the account's ID.
|
|
func deleteCredential(forAccountID accountID: UUID)
|
|
|
|
/// Rewrites stored credentials so they carry the current iCloud sync
|
|
/// setting. Whether a Keychain item syncs is decided when it is written, so
|
|
/// enabling sync later would otherwise leave existing logins local — and
|
|
/// every other device with an account whose credential never arrives.
|
|
func resyncCredentialsWithCurrentSetting()
|
|
|
|
/// The login identifier the user typed. Kept in the Keychain beside the
|
|
/// credential so it never enters synced metadata.
|
|
func setUsername(_ username: String, for account: Account)
|
|
func username(for account: Account) -> String?
|
|
|
|
func isLoggedIn(for account: Account) -> Bool
|
|
func refreshLoginStatus(for account: Account)
|
|
|
|
// MARK: - Legacy adoption
|
|
|
|
/// Whether a pre-revamp credential for this account's instance exists on
|
|
/// this device and has not been adopted by the current identity.
|
|
///
|
|
/// True only for the account the instance's legacy login was migrated into
|
|
/// — the old item is keyed by instance, and a second login the user added
|
|
/// themselves must never inherit it.
|
|
func hasAdoptableLegacyCredential(for account: Account) -> Bool
|
|
|
|
/// Copies the legacy item into this account's scoped key. Returns whether
|
|
/// the copy verified.
|
|
///
|
|
/// Runs by itself as part of recomputing account status: the upgrade is not
|
|
/// a decision to put to the user. A retained Keychain item carries no proof
|
|
/// of which Apple account it belonged to (measured — see
|
|
/// ``CredentialIdentityScope``), but the pre-revamp release read that item
|
|
/// with `kSecAttrSynchronizableAny` and no identity in its key, so it was
|
|
/// already being served to whoever held the device. The copy is written
|
|
/// device-local so this migration cannot put a secret of unproven
|
|
/// provenance into the current identity's iCloud Keychain.
|
|
@discardableResult
|
|
func adoptLegacyCredential(for account: Account) -> Bool
|
|
|
|
/// Deletes the unscoped legacy item **only** if this identity adopted it
|
|
/// and it still holds the adopted secret. Called when an account is really
|
|
/// deleted, never on mere absence.
|
|
func deleteLegacyCredentialIfOwned(for account: Account)
|
|
}
|