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); +}