Files
keyboard/Shared/KBAuthManager.m
2025-11-03 21:04:39 +08:00

212 lines
8.0 KiB
Objective-C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// KBAuthManager.m
//
// 关键点:
// - 使用固定的 service/account 将 KBAuthSession 序列化后保存到钥匙串;
// - 通过 kSecAttrAccessGroup 指定 Keychain Sharing 访问组,实现 App 与扩展共享;
// - 保存/清除时发送 Darwin 跨进程通知,便于对端刷新缓存;
//
#import "KBAuthManager.h"
#import <Security/Security.h>
#import "KBConfig.h" // 需要共享钥匙串访问组常量,见 KBConfig.h 中的说明
NSString * const kKBDarwinAuthChanged = @"com.loveKey.nyx.auth.changed";
NSNotificationName const KBAuthChangedNotification = @"KBAuthChangedNotification";
static NSString * const kKBKCService = @"com.loveKey.nyx.auth"; // 钥匙串 service 名
static NSString * const kKBKCAccount = @"session"; // 钥匙串 account 键
// 用于 Keychain Sharing 的访问组;必须与两个 target 的 entitlements 配置一致。
// 示例Capabilities 中勾选 Keychain Sharing 后的值):
// $(AppIdentifierPrefix)com.loveKey.nyx.shared
// 运行时会被展开为TN6HHV45BB.com.loveKey.nyx.shared
#ifndef KB_KEYCHAIN_ACCESS_GROUP
#define KB_KEYCHAIN_ACCESS_GROUP @"TN6HHV45BB.com.loveKey.nyx.shared"
#endif
// 过期宽限:若过期时间距离当前 <= 该阈值,则视为已过期。
static const NSTimeInterval kKBExpiryGrace = 5.0; // 秒
@implementation KBAuthSession
+ (BOOL)supportsSecureCoding { return YES; }
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:self.accessToken forKey:@"accessToken"];
[coder encodeObject:self.refreshToken forKey:@"refreshToken"];
[coder encodeObject:self.expiryDate forKey:@"expiryDate"];
[coder encodeObject:self.userIdentifier forKey:@"userIdentifier"];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super init]) {
_accessToken = [coder decodeObjectOfClass:NSString.class forKey:@"accessToken"];
_refreshToken = [coder decodeObjectOfClass:NSString.class forKey:@"refreshToken"];
_expiryDate = [coder decodeObjectOfClass:NSDate.class forKey:@"expiryDate"];
_userIdentifier = [coder decodeObjectOfClass:NSString.class forKey:@"userIdentifier"];
}
return self;
}
@end
@interface KBAuthManager ()
@property (atomic, strong, readwrite, nullable) KBAuthSession *current;
@end
@implementation KBAuthManager
+ (instancetype)shared {
static KBAuthManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBAuthManager new]; });
return m;
}
#if DEBUG
static inline void KBLog(NSString *fmt, ...) {
va_list args; va_start(args, fmt);
NSString *msg = [[NSString alloc] initWithFormat:fmt arguments:args];
va_end(args);
NSLog(@"[KBAuth] %@", msg);
}
#endif
- (instancetype)init {
if (self = [super init]) {
[self reloadFromKeychain];
// 监听 Darwin 跨进程通知App 与扩展之间)
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
KBAuthDarwinCallback,
(__bridge CFStringRef)kKBDarwinAuthChanged,
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
}
return self;
}
static void KBAuthDarwinCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
KBAuthManager *self = (__bridge KBAuthManager *)observer;
[self reloadFromKeychain];
}
- (void)dealloc {
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), (__bridge CFStringRef)kKBDarwinAuthChanged, NULL);
}
- (BOOL)isLoggedIn {
KBAuthSession *s = self.current;
if (s.accessToken.length == 0) return NO;
if (!s.expiryDate) return YES; // 未设置过期时间时,只要有 token 即视为已登录
return ([s.expiryDate timeIntervalSinceNow] > kKBExpiryGrace);
}
#pragma mark - Public
- (void)reloadFromKeychain {
NSData *data = [self keychainRead];
KBAuthSession *session = nil;
if (data.length > 0) {
@try {
session = [NSKeyedUnarchiver unarchivedObjectOfClass:KBAuthSession.class fromData:data error:NULL];
} @catch (__unused NSException *e) { session = nil; }
}
self.current = session;
[[NSNotificationCenter defaultCenter] postNotificationName:KBAuthChangedNotification object:nil]; // 进程内通知
}
- (BOOL)saveAccessToken:(NSString *)accessToken
refreshToken:(NSString *)refreshToken
expiryDate:(NSDate *)expiryDate
userIdentifier:(NSString *)userIdentifier {
KBAuthSession *s = [KBAuthSession new];
s.accessToken = accessToken ?: @"";
s.refreshToken = refreshToken;
s.expiryDate = expiryDate;
s.userIdentifier = userIdentifier;
NSError *err = nil;
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:s requiringSecureCoding:YES error:&err];
if (err || data.length == 0) return NO;
BOOL ok = [self keychainWrite:data];
if (ok) {
self.current = s;
// 进程内通知
[[NSNotificationCenter defaultCenter] postNotificationName:KBAuthChangedNotification object:nil];
// 跨进程通知App <-> 扩展)
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFStringRef)kKBDarwinAuthChanged, NULL, NULL, true);
}
return ok;
}
- (void)signOut {
[self keychainDelete];
self.current = nil;
[[NSNotificationCenter defaultCenter] postNotificationName:KBAuthChangedNotification object:nil];
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFStringRef)kKBDarwinAuthChanged, NULL, NULL, true);
}
- (NSDictionary<NSString *,NSString *> *)authorizationHeader {
NSString *t = self.current.accessToken;
if (t.length == 0) return @{}; // 未登录返回空头部
return @{ @"Authorization": [@"Bearer " stringByAppendingString:t] };
}
#pragma mark - Keychain (shared)
- (NSMutableDictionary *)baseKCQuery {
NSMutableDictionary *q = [@{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: kKBKCService,
(__bridge id)kSecAttrAccount: kKBKCAccount } mutableCopy];
// 指定共享访问组App 与扩展共用同一组)
q[(__bridge id)kSecAttrAccessGroup] = KB_KEYCHAIN_ACCESS_GROUP;
return q;
}
- (BOOL)keychainWrite:(NSData *)data {
if (!data) return NO;
NSMutableDictionary *query = [self baseKCQuery];
SecItemDelete((__bridge CFDictionaryRef)query);
// 设置属性
query[(__bridge id)kSecValueData] = data;
// 访问控制:设备首次解锁后可读,不随备份迁移
query[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
#if DEBUG
if (status != errSecSuccess) {
KBLog(@"SecItemAdd failed status=%ld group=%@", (long)status, KB_KEYCHAIN_ACCESS_GROUP);
} else {
KBLog(@"SecItemAdd ok group=%@", KB_KEYCHAIN_ACCESS_GROUP);
}
#endif
return (status == errSecSuccess);
}
- (NSData *)keychainRead {
NSMutableDictionary *query = [self baseKCQuery];
query[(__bridge id)kSecReturnData] = @YES;
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
CFTypeRef dataRef = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataRef);
#if DEBUG
if (status != errSecSuccess) {
KBLog(@"SecItemCopyMatching status=%ld group=%@ (item not found or no entitlement)", (long)status, KB_KEYCHAIN_ACCESS_GROUP);
} else {
KBLog(@"SecItemCopyMatching ok group=%@", KB_KEYCHAIN_ACCESS_GROUP);
}
#endif
if (status != errSecSuccess || !dataRef) return nil;
return (__bridge_transfer NSData *)dataRef;
}
- (void)keychainDelete {
NSDictionary *query = [self baseKCQuery];
SecItemDelete((__bridge CFDictionaryRef)query);
}
@end