Files
PojavLauncher_iOS/Natives/AccountListViewController.m
khanhduytran0 28df9fcc68 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.
2024-04-04 19:12:32 +07:00

286 lines
13 KiB
Objective-C

#import <AuthenticationServices/AuthenticationServices.h>
#import "authenticator/BaseAuthenticator.h"
#import "AccountListViewController.h"
#import "AFNetworking.h"
#import "LauncherPreferences.h"
#import "UIImageView+AFNetworking.h"
#import "ios_uikit_bridge.h"
#import "utils.h"
@interface AccountListViewController()<ASWebAuthenticationPresentationContextProviding>
@property(nonatomic, strong) NSMutableArray *accountList;
@property(nonatomic) ASWebAuthenticationSession *authVC;
@end
@implementation AccountListViewController
- (void)viewDidLoad {
[super viewDidLoad];
if (self.accountList == nil) {
self.accountList = [NSMutableArray array];
} else {
[self.accountList removeAllObjects];
}
// List accounts
NSString *listPath = [NSString stringWithFormat:@"%s/accounts", getenv("POJAV_HOME")];
NSFileManager *fm = [NSFileManager defaultManager];
NSArray *files = [fm contentsOfDirectoryAtPath:listPath error:nil];
for(NSString *file in files) {
NSString *path = [listPath stringByAppendingPathComponent:file];
BOOL isDir = NO;
[fm fileExistsAtPath:path isDirectory:(&isDir)];
if(!isDir && [file hasSuffix:@".json"]) {
[self.accountList addObject:parseJSONFromFile(path)];
}
}
[self.tableView setSeparatorStyle:UITableViewCellSeparatorStyleSingleLine];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.accountList.count + 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"];
}
if (indexPath.row == self.accountList.count) {
cell.imageView.image = [UIImage imageNamed:@"IconAdd"];
cell.textLabel.text = localize(@"login.option.add", nil);
return cell;
}
NSDictionary *selected = self.accountList[indexPath.row];
// By default, display the saved username
cell.textLabel.text = selected[@"username"];
if ([selected[@"username"] hasPrefix:@"Demo."]) {
// Remove the prefix "Demo."
cell.textLabel.text = [selected[@"username"] substringFromIndex:5];
cell.detailTextLabel.text = localize(@"login.option.demo", nil);
} else if (selected[@"xboxGamertag"] == nil) {
cell.detailTextLabel.text = localize(@"login.option.local", nil);
} else {
// Display the Xbox gamertag for online accounts
cell.detailTextLabel.text = selected[@"xboxGamertag"];
}
cell.imageView.contentMode = UIViewContentModeCenter;
[cell.imageView setImageWithURL:[NSURL URLWithString:[selected[@"profilePicURL"] stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"]] placeholderImage:[UIImage imageNamed:@"DefaultAccount"]];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:NO];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if (indexPath.row == self.accountList.count) {
[self actionAddAccount:cell];
return;
}
self.modalInPresentation = YES;
self.tableView.userInteractionEnabled = NO;
[self addActivityIndicatorTo:cell];
id callback = ^(id status, BOOL success) {
[self callbackMicrosoftAuth:status success:success forCell:cell];
};
[[BaseAuthenticator loadSavedName:self.accountList[indexPath.row][@"username"]] refreshTokenWithCallback:callback];
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// TODO: invalidate token
NSString *str = self.accountList[indexPath.row][@"username"];
NSFileManager *fm = [NSFileManager defaultManager];
NSString *path = [NSString stringWithFormat:@"%s/accounts/%@.json", getenv("POJAV_HOME"), str];
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];
}
}
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == self.accountList.count) {
return UITableViewCellEditingStyleNone;
} else {
return UITableViewCellEditingStyleDelete;
}
}
- (NSDictionary *)parseQueryItems:(NSString *)url {
NSMutableDictionary *result = [NSMutableDictionary new];
NSArray<NSURLQueryItem *> *queryItems = [NSURLComponents componentsWithString:url].queryItems;
for (NSURLQueryItem *item in queryItems) {
result[item.name] = item.value;
}
return result;
}
- (void)actionAddAccount:(UITableViewCell *)sender {
UIAlertController *picker = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
UIAlertAction *actionMicrosoft = [UIAlertAction actionWithTitle:localize(@"login.option.microsoft", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[self actionLoginMicrosoft:sender];
}];
[picker addAction:actionMicrosoft];
UIAlertAction *actionLocal = [UIAlertAction actionWithTitle:localize(@"login.option.local", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[self actionLoginLocal:sender];
}];
[picker addAction:actionLocal];
UIAlertAction *cancel = [UIAlertAction actionWithTitle:localize(@"Cancel", nil) style:UIAlertActionStyleCancel handler:nil];
[picker addAction:cancel];
picker.popoverPresentationController.sourceView = sender;
picker.popoverPresentationController.sourceRect = sender.bounds;
[self presentViewController:picker animated:YES completion:nil];
}
- (void)actionLoginLocal:(UIView *)sender {
if (getPrefBool(@"warnings.local_warn")) {
setPrefBool(@"warnings.local_warn", NO);
UIAlertController *alert = [UIAlertController alertControllerWithTitle:localize(@"login.warn.title.localmode", nil) message:localize(@"login.warn.message.localmode", nil) preferredStyle:UIAlertControllerStyleActionSheet];
alert.popoverPresentationController.sourceView = sender;
alert.popoverPresentationController.sourceRect = sender.bounds;
UIAlertAction *ok = [UIAlertAction actionWithTitle:localize(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {[self actionLoginLocal:sender];}];
[alert addAction:ok];
[self presentViewController:alert animated:YES completion:nil];
return;
}
UIAlertController *controller = [UIAlertController alertControllerWithTitle:localize(@"Sign in", nil) message:localize(@"login.option.local", nil) preferredStyle:UIAlertControllerStyleAlert];
[controller addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = localize(@"login.alert.field.username", nil);
textField.clearButtonMode = UITextFieldViewModeWhileEditing;
textField.borderStyle = UITextBorderStyleRoundedRect;
}];
[controller addAction:[UIAlertAction actionWithTitle:localize(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
NSArray *textFields = controller.textFields;
UITextField *usernameField = textFields[0];
if (usernameField.text.length < 3 || usernameField.text.length > 16) {
controller.message = localize(@"login.error.username.outOfRange", nil);
[self presentViewController:controller animated:YES completion:nil];
} else {
id callback = ^(id status, BOOL success) {
self.whenItemSelected();
[self dismissViewControllerAnimated:YES completion:nil];
};
[[[LocalAuthenticator alloc] initWithInput:usernameField.text] loginWithCallback:callback];
}
}]];
[controller addAction:[UIAlertAction actionWithTitle:localize(@"Cancel", nil) style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:controller animated:YES completion:nil];
}
- (void)actionLoginMicrosoft:(UITableViewCell *)sender {
NSURL *url = [NSURL URLWithString:@"https://login.live.com/oauth20_authorize.srf?client_id=00000000402b5328&response_type=code&scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL&redirect_url=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf"];
self.authVC =
[[ASWebAuthenticationSession alloc] initWithURL:url
callbackURLScheme:@"ms-xal-00000000402b5328"
completionHandler:^(NSURL * _Nullable callbackURL, NSError * _Nullable error)
{
if (callbackURL == nil) {
if (error.code != ASWebAuthenticationSessionErrorCodeCanceledLogin) {
showDialog(localize(@"Error", nil), error.localizedDescription);
}
return;
}
// NSLog(@"URL returned = %@", [callbackURL absoluteString]);
NSDictionary *queryItems = [self parseQueryItems:callbackURL.absoluteString];
if (queryItems[@"code"]) {
self.modalInPresentation = YES;
self.tableView.userInteractionEnabled = NO;
[self addActivityIndicatorTo:sender];
id callback = ^(id status, BOOL success) {
if ([status isKindOfClass:NSString.class] && [status isEqualToString:@"DEMO"] && success) {
showDialog(localize(@"login.warn.title.demomode", nil), localize(@"login.warn.message.demomode", nil));
}
[self callbackMicrosoftAuth:status success:success forCell:sender];
};
[[[MicrosoftAuthenticator alloc] initWithInput:queryItems[@"code"]] loginWithCallback:callback];
} else {
if ([queryItems[@"error"] hasPrefix:@"access_denied"]) {
// Ignore access denial responses
return;
}
showDialog(localize(@"Error", nil), queryItems[@"error_description"]);
}
}];
self.authVC.prefersEphemeralWebBrowserSession = YES;
self.authVC.presentationContextProvider = self;
if ([self.authVC start] == NO) {
showDialog(localize(@"Error", nil), @"Unable to open Safari");
}
}
- (void)addActivityIndicatorTo:(UITableViewCell *)cell {
UIActivityIndicatorViewStyle indicatorStyle = UIActivityIndicatorViewStyleMedium;
UIActivityIndicatorView *indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:indicatorStyle];
cell.accessoryView = indicator;
[indicator sizeToFit];
[indicator startAnimating];
}
- (void)removeActivityIndicatorFrom:(UITableViewCell *)cell {
UIActivityIndicatorView *indicator = (id)cell.accessoryView;
[indicator stopAnimating];
cell.accessoryView = nil;
}
- (void)callbackMicrosoftAuth:(id)status success:(BOOL)success forCell:(UITableViewCell *)cell {
if (status != nil) {
if (success) {
cell.detailTextLabel.text = status;
} else {
self.modalInPresentation = NO;
self.tableView.userInteractionEnabled = YES;
[self removeActivityIndicatorFrom:cell];
cell.detailTextLabel.text = [status localizedDescription];
NSData *errorData = ((NSError *)status).userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
NSString *errorStr = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding];
NSLog(@"[MSA] Error: %@", errorStr);
showDialog(localize(@"Error", nil), errorStr);
}
} else if (success) {
self.whenItemSelected();
[self removeActivityIndicatorFrom:cell];
[self dismissViewControllerAnimated:YES completion:nil];
}
}
#pragma mark - UIPopoverPresentationControllerDelegate
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller traitCollection:(UITraitCollection *)traitCollection {
return UIModalPresentationNone;
}
#pragma mark - ASWebAuthenticationPresentationContextProviding
- (ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session {
return UIApplication.sharedApplication.windows.firstObject;
}
@end