From 28df9fcc683e0af791bcb9de1823bb5aaadc0c10 Mon Sep 17 00:00:00 2001 From: khanhduytran0 Date: Thu, 4 Apr 2024 19:12:32 +0700 Subject: [PATCH] Feat[auth]: move tokens to keychain Previously, tokens were saved in json under plain text, making it insecure. Now, they are stored in iOS's keychian with the hope of making it more secure. --- .../pojavlaunch/value/MinecraftAccount.java | 18 ++-- Natives/AccountListViewController.m | 4 + Natives/authenticator/BaseAuthenticator.h | 4 + Natives/authenticator/BaseAuthenticator.m | 4 +- Natives/authenticator/LocalAuthenticator.m | 3 +- .../authenticator/MicrosoftAuthenticator.m | 95 ++++++++++++++++++- 6 files changed, 109 insertions(+), 19 deletions(-) diff --git a/JavaApp/src/main/java/net/kdt/pojavlaunch/value/MinecraftAccount.java b/JavaApp/src/main/java/net/kdt/pojavlaunch/value/MinecraftAccount.java index f5125680..559db0de 100644 --- a/JavaApp/src/main/java/net/kdt/pojavlaunch/value/MinecraftAccount.java +++ b/JavaApp/src/main/java/net/kdt/pojavlaunch/value/MinecraftAccount.java @@ -21,7 +21,10 @@ public class MinecraftAccount } public static MinecraftAccount parse(String content) throws JsonSyntaxException { - return Tools.GLOBAL_GSON.fromJson(content, MinecraftAccount.class); + MinecraftAccount account = Tools.GLOBAL_GSON.fromJson(content, MinecraftAccount.class); + // Read access token from keychain + account.accessToken = getAccessTokenFromKeychain(account.xuid); + return account; } public static MinecraftAccount load(String name) throws IOException, JsonSyntaxException { @@ -39,16 +42,9 @@ public class MinecraftAccount } return acc; } -/* - public static void clearTempAccount() { - File tempAccFile = new File(Tools.DIR_BUNDLE, "cache/tempacc.json"); - tempAccFile.delete(); - } - public static void saveTempAccount(MinecraftAccount acc) throws IOException { - File tempAccFile = new File(Tools.DIR_DATA, "cache/tempacc.json"); - tempAccFile.delete(); - acc.save(tempAccFile.getAbsolutePath()); + static { + System.load(System.getenv("BUNDLE_PATH") + "/PojavLauncher"); } -*/ + public static native String getAccessTokenFromKeychain(String xuid); } diff --git a/Natives/AccountListViewController.m b/Natives/AccountListViewController.m index 3165fe91..c3c63027 100644 --- a/Natives/AccountListViewController.m +++ b/Natives/AccountListViewController.m @@ -110,6 +110,10 @@ if (self.whenDelete != nil) { self.whenDelete(str); } + NSString *xuid = self.accountList[indexPath.row][@"xuid"]; + if (xuid) { + [MicrosoftAuthenticator clearTokenDataOfProfile:xuid]; + } [fm removeItemAtPath:path error:nil]; [self.accountList removeObjectAtIndex:indexPath.row]; [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; diff --git a/Natives/authenticator/BaseAuthenticator.h b/Natives/authenticator/BaseAuthenticator.h index 647b324f..130a75f6 100644 --- a/Natives/authenticator/BaseAuthenticator.h +++ b/Natives/authenticator/BaseAuthenticator.h @@ -9,6 +9,7 @@ typedef void(^Callback)(id status, BOOL success); @property NSMutableDictionary *authData; + (id)loadSavedName:(NSString *)name; ++ (NSDictionary *)tokenDataOfProfile:(NSString *)profile; - (id)initWithInput:(NSString *)string; - (void)loginWithCallback:(Callback)callback; @@ -21,4 +22,7 @@ typedef void(^Callback)(id status, BOOL success); @end @interface MicrosoftAuthenticator : BaseAuthenticator + ++ (void)clearTokenDataOfProfile:(NSString *)profile; + @end diff --git a/Natives/authenticator/BaseAuthenticator.m b/Natives/authenticator/BaseAuthenticator.m index 4cbf3888..1bae8029 100644 --- a/Natives/authenticator/BaseAuthenticator.m +++ b/Natives/authenticator/BaseAuthenticator.m @@ -1,3 +1,4 @@ +#import #import "BaseAuthenticator.h" #import "../LauncherPreferences.h" #import "../ios_uikit_bridge.h" @@ -27,7 +28,8 @@ static BaseAuthenticator *current = nil; } return nil; } - if ([authData[@"accessToken"] length] < 5) { + + if ([authData[@"expiresAt"] longValue] == 0) { return [[LocalAuthenticator alloc] initWithData:authData]; } else { return [[MicrosoftAuthenticator alloc] initWithData:authData]; diff --git a/Natives/authenticator/LocalAuthenticator.m b/Natives/authenticator/LocalAuthenticator.m index 3d6a0a5c..15e288f7 100644 --- a/Natives/authenticator/LocalAuthenticator.m +++ b/Natives/authenticator/LocalAuthenticator.m @@ -3,8 +3,7 @@ @implementation LocalAuthenticator - (void)loginWithCallback:(Callback)callback { - self.authData[@"oldusername"] = self.authData[@"username"] = self.authData[@"input"]; - self.authData[@"accessToken"] = @"0"; + self.authData[@"oldusername"] = self.authData[@"username"] = self.authData[@"input"]; self.authData[@"profileId"] = @"00000000-0000-0000-0000-000000000000"; callback(nil, [super saveChanges]); } diff --git a/Natives/authenticator/MicrosoftAuthenticator.m b/Natives/authenticator/MicrosoftAuthenticator.m index c08afab5..75f32eb0 100644 --- a/Natives/authenticator/MicrosoftAuthenticator.m +++ b/Natives/authenticator/MicrosoftAuthenticator.m @@ -2,6 +2,7 @@ #import "BaseAuthenticator.h" #import "../ios_uikit_bridge.h" #import "../utils.h" +#include "jni.h" typedef void(^XSTSCallback)(NSString *xsts, NSString *uhs); @@ -185,7 +186,7 @@ typedef void(^XSTSCallback)(NSString *xsts, NSString *uhs); self.authData[@"profilePicURL"] = [NSString stringWithFormat:@"https://mc-heads.net/head/%@/120", self.authData[@"profileId"]]; self.authData[@"oldusername"] = self.authData[@"username"]; self.authData[@"username"] = response[@"name"]; - callback(nil, [super saveChanges]); + callback(nil, [self saveChanges]); } failure:^(NSURLSessionDataTask *task, NSError *error) { NSData *errorData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; NSDictionary *errorDict = [NSJSONSerialization JSONObjectWithData: errorData options:kNilOptions error:nil]; @@ -194,12 +195,16 @@ typedef void(^XSTSCallback)(NSString *xsts, NSString *uhs); self.authData[@"profileId"] = @"00000000-0000-0000-0000-000000000000"; self.authData[@"username"] = [NSString stringWithFormat:@"Demo.%@", self.authData[@"xboxGamertag"]]; - callback(@"DEMO", [super saveChanges]); - callback(nil, YES); + if ([self saveChanges]) { + callback(@"DEMO", YES); + callback(nil, YES); + } else { + callback(nil, NO); + } return; } - callback([[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding], NO); + callback(error, NO); }]; } @@ -208,11 +213,91 @@ typedef void(^XSTSCallback)(NSString *xsts, NSString *uhs); } - (void)refreshTokenWithCallback:(Callback)callback { + // Move tokens to keychain if we haven't + if (!self.tokenData) { + [self saveChanges]; + } + if ([NSDate.date timeIntervalSince1970] > [self.authData[@"expiresAt"] longValue]) { - [self acquireAccessToken:self.authData[@"msaRefreshToken"] refresh:YES callback:callback]; + [self acquireAccessToken:self.tokenData[@"refreshToken"] refresh:YES callback:callback]; } else { callback(nil, YES); } } +- (BOOL)saveChanges { + BOOL savedToKeychain = [self setAccessToken:self.authData[@"accessToken"] refreshToken:self.authData[@"msaRefreshToken"]]; + if (!savedToKeychain) { + showDialog(localize(@"Error", nil), @"Failed to save account tokens to keychain"); + return NO; + } + [self.authData removeObjectsForKeys:@[@"accessToken", @"msaRefreshToken"]]; + return [super saveChanges]; +} + +#pragma mark Keychain + ++ (NSDictionary *)keychainQueryForKey:(NSString *)profile extraInfo:(NSDictionary *)extra { + NSMutableDictionary *dict = @{ + (id)kSecClass: (id)kSecClassGenericPassword, + (id)kSecAttrService: @"AccountToken", + (id)kSecAttrAccount: profile, + (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly + }.mutableCopy; + if (extra) { + [dict addEntriesFromDictionary:extra]; + } + return dict; +} + ++ (NSDictionary *)tokenDataOfProfile:(NSString *)profile { + NSDictionary *dict = [MicrosoftAuthenticator keychainQueryForKey:profile extraInfo:@{ + (id)kSecMatchLimit: (id)kSecMatchLimitOne, + (id)kSecReturnData: (id)kCFBooleanTrue + }]; + CFTypeRef result = nil; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)dict, &result); + if (status == errSecSuccess) { + return [NSKeyedUnarchiver unarchivedObjectOfClass:NSDictionary.class fromData:(__bridge NSData *)result error:nil]; + } else { + return nil; + } +} + ++ (void)clearTokenDataOfProfile:(NSString *)profile { + NSDictionary *dict = [MicrosoftAuthenticator keychainQueryForKey:profile extraInfo:nil]; + SecItemDelete((__bridge CFDictionaryRef)dict); +} + +- (BOOL)setAccessToken:(NSString *)accessToken refreshToken:(NSString *)refreshToken { + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:@{ + @"accessToken": accessToken, + @"refreshToken": refreshToken ?: self.tokenData[@"refreshToken"] + } requiringSecureCoding:YES error:nil]; + NSDictionary *dict = [MicrosoftAuthenticator keychainQueryForKey:self.authData[@"xuid"] extraInfo:@{ + (id)kSecValueData: data + }]; + SecItemDelete((__bridge CFDictionaryRef)dict); + OSStatus status = SecItemAdd((__bridge CFDictionaryRef)dict, NULL); + return status == errSecSuccess; +} + +- (NSDictionary *)tokenData { + return [MicrosoftAuthenticator tokenDataOfProfile:self.authData[@"xuid"]]; +} + @end + +JNIEXPORT jstring JNICALL Java_net_kdt_pojavlaunch_value_MinecraftAccount_getAccessTokenFromKeychain(JNIEnv *env, jclass clazz, jstring xuid) { + // This function should only be called once + static BOOL called = NO; + if (called) { + abort(); + } + called = YES; + + const char *xuidC = (*env)->GetStringUTFChars(env, xuid, 0); + NSString *accessToken = [MicrosoftAuthenticator tokenDataOfProfile:@(xuidC)][@"accessToken"]; + (*env)->ReleaseStringUTFChars(env, xuid, xuidC); + return (*env)->NewStringUTF(env, accessToken.UTF8String); +}