Skip to content

Commit

Permalink
Feat[auth]: move tokens to keychain
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
khanhduytran0 committed Apr 4, 2024
1 parent 208503d commit 28df9fc
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ public String save() throws IOException {
}

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 {
Expand All @@ -39,16 +42,9 @@ public static MinecraftAccount load(String name) throws IOException, JsonSyntaxE
}
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);
}
4 changes: 4 additions & 0 deletions Natives/AccountListViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEd
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];
Expand Down
4 changes: 4 additions & 0 deletions Natives/authenticator/BaseAuthenticator.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,4 +22,7 @@ typedef void(^Callback)(id status, BOOL success);
@end

@interface MicrosoftAuthenticator : BaseAuthenticator

+ (void)clearTokenDataOfProfile:(NSString *)profile;

@end
4 changes: 3 additions & 1 deletion Natives/authenticator/BaseAuthenticator.m
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#import <Security/Security.h>
#import "BaseAuthenticator.h"
#import "../LauncherPreferences.h"
#import "../ios_uikit_bridge.h"
Expand Down Expand Up @@ -27,7 +28,8 @@ + (id)loadSavedName:(NSString *)name {
}
return nil;
}
if ([authData[@"accessToken"] length] < 5) {

if ([authData[@"expiresAt"] longValue] == 0) {
return [[LocalAuthenticator alloc] initWithData:authData];
} else {
return [[MicrosoftAuthenticator alloc] initWithData:authData];
Expand Down
3 changes: 1 addition & 2 deletions Natives/authenticator/LocalAuthenticator.m
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand Down
95 changes: 90 additions & 5 deletions Natives/authenticator/MicrosoftAuthenticator.m
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -185,7 +186,7 @@ - (void)checkMCProfile:(NSString *)mcAccessToken callback:(Callback)callback {
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];
Expand All @@ -194,12 +195,16 @@ - (void)checkMCProfile:(NSString *)mcAccessToken callback:(Callback)callback {
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);
}];
}

Expand All @@ -208,11 +213,91 @@ - (void)loginWithCallback:(Callback)callback {
}

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

0 comments on commit 28df9fc

Please sign in to comment.