Skip to content

Commit 78569b8

Browse files
lolgearlucasderraugh
authored andcommitted
Windows Separation Remastered: Authentication Window Controller (#607)
1 parent 0eb04c6 commit 78569b8

11 files changed

+489
-267
lines changed

GitUp/Application/AppDelegate.h

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
// You should have received a copy of the GNU General Public License
1414
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1515

16-
#import <GitUpKit/GitUpKit.h>
16+
#import <AppKit/AppKit.h>
1717

18-
@interface AppDelegate : NSObject <NSApplicationDelegate, GCRepositoryDelegate>
18+
@interface AppDelegate : NSObject <NSApplicationDelegate>
1919
@property(nonatomic, strong) IBOutlet NSWindow* preferencesWindow;
2020
@property(nonatomic, weak) IBOutlet NSToolbar* preferencesToolbar;
2121
@property(nonatomic, weak) IBOutlet NSTabView* preferencesTabView;
@@ -26,18 +26,7 @@
2626
@property(nonatomic, weak) IBOutlet NSTextField* cloneURLTextField;
2727
@property(nonatomic, weak) IBOutlet NSButton* cloneRecursiveButton;
2828

29-
@property(nonatomic, strong) IBOutlet NSWindow* authenticationWindow;
30-
@property(nonatomic, weak) IBOutlet NSTextField* authenticationURLTextField;
31-
@property(nonatomic, weak) IBOutlet NSTextField* authenticationNameTextField;
32-
@property(nonatomic, weak) IBOutlet NSSecureTextField* authenticationPasswordTextField;
33-
3429
+ (instancetype)sharedDelegate;
35-
+ (BOOL)loadPlainTextAuthenticationFormKeychainForURL:(NSURL*)url user:(NSString*)user username:(NSString**)username password:(NSString**)password allowInteraction:(BOOL)allowInteraction;
36-
+ (void)savePlainTextAuthenticationToKeychainForURL:(NSURL*)url withUsername:(NSString*)username password:(NSString*)password;
37-
38-
- (void)repository:(GCRepository*)repository willStartTransferWithURL:(NSURL*)url;
39-
- (BOOL)repository:(GCRepository*)repository requiresPlainTextAuthenticationForURL:(NSURL*)url user:(NSString*)user username:(NSString**)username password:(NSString**)password;
40-
- (void)repository:(GCRepository*)repository didFinishTransferWithURL:(NSURL*)url success:(BOOL)success;
4130

4231
- (void)handleDocumentCountChanged;
4332
@end

GitUp/Application/AppDelegate.m

Lines changed: 1 addition & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@
1313
// You should have received a copy of the GNU General Public License
1414
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1515

16-
#import <Security/Security.h>
1716
#pragma clang diagnostic push
1817
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
1918
#import <HockeySDK/HockeySDK.h>
2019
#pragma clang diagnostic pop
2120
#import <Sparkle/Sparkle.h>
2221

22+
#import <GitUpKit/GitUpKit.h>
2323
#import <GitUpKit/XLFacilityMacros.h>
2424

2525
#import "AppDelegate.h"
@@ -52,11 +52,6 @@ @implementation AppDelegate {
5252
BOOL _updatePending;
5353
BOOL _manualCheck;
5454

55-
BOOL _authenticationUseKeychain;
56-
NSURL* _authenticationURL;
57-
NSString* _authenticationUsername;
58-
NSString* _authenticationPassword;
59-
6055
CFMessagePortRef _messagePort;
6156
}
6257

@@ -103,89 +98,6 @@ + (instancetype)sharedDelegate {
10398
return (AppDelegate*)[NSApp delegate];
10499
}
105100

106-
// WARNING: We are using the same attributes for the keychain items than Git CLT appears to be using as of version 1.9.3
107-
+ (BOOL)loadPlainTextAuthenticationFormKeychainForURL:(NSURL*)url user:(NSString*)user username:(NSString**)username password:(NSString**)password allowInteraction:(BOOL)allowInteraction {
108-
const char* serverName = url.host.UTF8String;
109-
if (serverName && serverName[0]) { // TODO: How can this be NULL?
110-
const char* accountName = (*username).UTF8String;
111-
SecKeychainItemRef itemRef;
112-
UInt32 passwordLength;
113-
void* passwordData;
114-
SecKeychainSetUserInteractionAllowed(allowInteraction); // Ignore errors
115-
OSStatus status = SecKeychainFindInternetPassword(NULL,
116-
(UInt32)strlen(serverName), serverName,
117-
0, NULL, // Any security domain
118-
accountName ? (UInt32)strlen(accountName) : 0, accountName,
119-
0, NULL, // Any path
120-
0, // Any port
121-
kSecProtocolTypeAny,
122-
kSecAuthenticationTypeAny,
123-
&passwordLength, &passwordData, &itemRef);
124-
if (status == noErr) {
125-
BOOL success = NO;
126-
*password = [[NSString alloc] initWithBytes:passwordData length:passwordLength encoding:NSUTF8StringEncoding];
127-
if (accountName == NULL) {
128-
UInt32 tag = kSecAccountItemAttr;
129-
UInt32 format = CSSM_DB_ATTRIBUTE_FORMAT_STRING;
130-
SecKeychainAttributeInfo info = {1, &tag, &format};
131-
SecKeychainAttributeList* attributes;
132-
status = SecKeychainItemCopyAttributesAndData(itemRef, &info, NULL, &attributes, NULL, NULL);
133-
if (status == noErr) {
134-
XLOG_DEBUG_CHECK(attributes->count == 1);
135-
XLOG_DEBUG_CHECK(attributes->attr[0].tag == kSecAccountItemAttr);
136-
*username = [[NSString alloc] initWithBytes:attributes->attr[0].data length:attributes->attr[0].length encoding:NSUTF8StringEncoding];
137-
success = YES;
138-
SecKeychainItemFreeAttributesAndData(attributes, NULL);
139-
} else {
140-
XLOG_ERROR(@"SecKeychainItemCopyAttributesAndData() returned error %i", status);
141-
}
142-
} else {
143-
success = YES;
144-
}
145-
SecKeychainItemFreeContent(NULL, passwordData);
146-
CFRelease(itemRef);
147-
if (success) {
148-
return YES;
149-
}
150-
} else if (status != errSecItemNotFound) {
151-
XLOG_ERROR(@"SecKeychainFindInternetPassword() returned error %i", status);
152-
}
153-
} else {
154-
XLOG_WARNING(@"Unable to extract hostname from remote URL: %@", url);
155-
}
156-
return NO;
157-
}
158-
159-
+ (void)savePlainTextAuthenticationToKeychainForURL:(NSURL*)url withUsername:(NSString*)username password:(NSString*)password {
160-
SecProtocolType type;
161-
if ([url.scheme isEqualToString:@"http"]) {
162-
type = kSecProtocolTypeHTTP;
163-
} else if ([url.scheme isEqualToString:@"https"]) {
164-
type = kSecProtocolTypeHTTPS;
165-
} else {
166-
XLOG_DEBUG_UNREACHABLE();
167-
return;
168-
}
169-
const char* serverName = url.host.UTF8String;
170-
const char* accountName = username.UTF8String;
171-
const char* accountPassword = password.UTF8String;
172-
SecKeychainSetUserInteractionAllowed(true); // Ignore errors
173-
OSStatus status = SecKeychainAddInternetPassword(NULL,
174-
(UInt32)strlen(serverName), serverName,
175-
0, NULL, // Any security domain
176-
accountName ? (UInt32)strlen(accountName) : 0, accountName,
177-
0, NULL, // Any path
178-
0, // Any port
179-
type,
180-
kSecAuthenticationTypeAny,
181-
(UInt32)strlen(accountPassword), accountPassword, NULL);
182-
if (status != noErr) {
183-
XLOG_ERROR(@"SecKeychainAddInternetPassword() returned error %i", status);
184-
} else {
185-
XLOG_VERBOSE(@"Successfully saved authentication in Keychain");
186-
}
187-
}
188-
189101
- (void)_setDocumentWindowModeID:(NSArray*)arguments {
190102
[(Document*)arguments[0] setWindowModeID:[arguments[1] unsignedIntegerValue]];
191103
}
@@ -704,49 +616,6 @@ - (void)userNotificationCenter:(NSUserNotificationCenter*)center didActivateNoti
704616
}
705617
}
706618

707-
#pragma mark - GCRepositoryDelegate
708-
709-
- (void)repository:(GCRepository*)repository willStartTransferWithURL:(NSURL*)url {
710-
_authenticationUseKeychain = YES;
711-
_authenticationURL = nil;
712-
_authenticationUsername = nil;
713-
_authenticationPassword = nil;
714-
}
715-
716-
- (BOOL)repository:(GCRepository*)repository requiresPlainTextAuthenticationForURL:(NSURL*)url user:(NSString*)user username:(NSString**)username password:(NSString**)password {
717-
if (_authenticationUseKeychain) {
718-
_authenticationUseKeychain = NO;
719-
if ([self.class loadPlainTextAuthenticationFormKeychainForURL:url user:user username:username password:password allowInteraction:YES]) {
720-
return YES;
721-
}
722-
} else {
723-
XLOG_VERBOSE(@"Skipping Keychain lookup for repeated authentication failures");
724-
}
725-
726-
_authenticationURLTextField.stringValue = url.absoluteString;
727-
_authenticationNameTextField.stringValue = *username ? *username : @"";
728-
_authenticationPasswordTextField.stringValue = @"";
729-
[_authenticationWindow makeFirstResponder:(*username ? _authenticationPasswordTextField : _authenticationNameTextField)];
730-
if ([NSApp runModalForWindow:_authenticationWindow] && _authenticationNameTextField.stringValue.length && _authenticationPasswordTextField.stringValue.length) {
731-
_authenticationURL = url;
732-
_authenticationUsername = [_authenticationNameTextField.stringValue copy];
733-
_authenticationPassword = [_authenticationPasswordTextField.stringValue copy];
734-
*username = _authenticationNameTextField.stringValue;
735-
*password = _authenticationPasswordTextField.stringValue;
736-
return YES;
737-
}
738-
return NO;
739-
}
740-
741-
- (void)repository:(GCRepository*)repository didFinishTransferWithURL:(NSURL*)url success:(BOOL)success {
742-
if (success && _authenticationURL && _authenticationUsername && _authenticationPassword) {
743-
[self.class savePlainTextAuthenticationToKeychainForURL:_authenticationURL withUsername:_authenticationUsername password:_authenticationPassword];
744-
}
745-
_authenticationURL = nil;
746-
_authenticationUsername = nil;
747-
_authenticationPassword = nil;
748-
}
749-
750619
#pragma mark - SUUpdaterDelegate
751620

752621
- (NSString*)feedURLStringForUpdater:(SUUpdater*)updater {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// AuthenticationWindowController.h
3+
// Application
4+
//
5+
// Created by Dmitry Lobanov on 08.10.2019.
6+
//
7+
8+
#import <AppKit/AppKit.h>
9+
10+
11+
NS_ASSUME_NONNULL_BEGIN
12+
13+
@class GCRepository;
14+
15+
@interface AuthenticationWindowController : NSWindowController
16+
17+
// Repository
18+
- (void)repository:(GCRepository*)repository willStartTransferWithURL:(NSURL*)url;
19+
- (BOOL)repository:(GCRepository*)repository requiresPlainTextAuthenticationForURL:(NSURL*)url user:(NSString*)user username:(NSString * _Nullable * _Nonnull )username password:(NSString * _Nullable * _Nonnull)password;
20+
- (void)repository:(GCRepository*)repository didFinishTransferWithURL:(NSURL*)url success:(BOOL)success;
21+
@end
22+
23+
NS_ASSUME_NONNULL_END
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
//
2+
// AuthenticationWindowController.m
3+
// Application
4+
//
5+
// Created by Dmitry Lobanov on 08.10.2019.
6+
//
7+
8+
#import "AuthenticationWindowController.h"
9+
#import <GitUpKit/GitUpKit.h>
10+
#import <GitUpKit/XLFacilityMacros.h>
11+
#import "KeychainAccessor.h"
12+
13+
@interface AuthenticationWindowControllerModel : NSObject
14+
@property (nonatomic, assign) BOOL useKeychain;
15+
@property (nonatomic, copy) NSURL *url;
16+
@property (nonatomic, copy) NSString *name;
17+
@property (nonatomic, copy) NSString *password;
18+
19+
@property (nonatomic, assign, readonly) BOOL isValid;
20+
@end
21+
22+
@implementation AuthenticationWindowControllerModel
23+
- (BOOL)isValid {
24+
return self.url && self.name && self.password;
25+
}
26+
- (void)unsetUseKeychain {
27+
self.useKeychain = NO;
28+
}
29+
- (void)willStartTransfer {
30+
self.useKeychain = YES;
31+
self.url = nil;
32+
self.name = nil;
33+
self.password = nil;
34+
}
35+
- (void)didFinishTransferWithURL:(NSURL *)url success:(BOOL)success onResult:(void(^)(AuthenticationWindowControllerModel *model))onResult {
36+
if (onResult) {
37+
BOOL shouldPassModel = success && self.isValid;
38+
onResult( shouldPassModel ? self : nil );
39+
}
40+
self.url = nil;
41+
self.name = nil;
42+
self.password = nil;
43+
}
44+
@end
45+
46+
@interface AuthenticationWindowController ()
47+
48+
// Model
49+
@property (nonatomic, strong) AuthenticationWindowControllerModel *model;
50+
51+
// Outlets
52+
@property(nonatomic, weak) IBOutlet NSTextField* urlTextField;
53+
@property(nonatomic, weak) IBOutlet NSTextField* nameTextField;
54+
@property(nonatomic, weak) IBOutlet NSSecureTextField* passwordTextField;
55+
56+
// Credentials
57+
@property (nonatomic, assign, readonly) BOOL credentialsExists;
58+
59+
@end
60+
61+
@implementation AuthenticationWindowController
62+
#pragma mark - Initialization
63+
- (instancetype)init {
64+
if ((self = [super initWithWindowNibName:@"AuthenticationWindowController"])) {
65+
self.model = [[AuthenticationWindowControllerModel alloc] init];
66+
}
67+
return self;
68+
}
69+
70+
#pragma mark - Window Lifecycle
71+
- (void)beforeRunInModal {
72+
self.urlTextField.stringValue = self.model.url.absoluteString;
73+
self.nameTextField.stringValue = self.model.name;
74+
self.passwordTextField.stringValue = self.model.password;
75+
[self makeFirstResponderWhenUsernameExists:self.model.name.length > 0];
76+
}
77+
- (void)windowDidLoad {
78+
[super windowDidLoad];
79+
[self beforeRunInModal];
80+
}
81+
82+
#pragma mark - Actions
83+
- (IBAction)dismissModal:(id)sender {
84+
[NSApp stopModalWithCode:[(NSButton *)sender tag]];
85+
[self close];
86+
}
87+
88+
#pragma mark - FirstResponder
89+
- (NSResponder *)firstResponderWhenUsernameExists:(BOOL)usernameExists {
90+
return usernameExists ? self.passwordTextField : self.nameTextField;
91+
}
92+
93+
- (void)makeFirstResponderWhenUsernameExists:(BOOL)usernameExists {
94+
[self.window makeFirstResponder:[self firstResponderWhenUsernameExists:usernameExists]];
95+
}
96+
97+
#pragma mark - Credentials
98+
- (BOOL)credentialsExists {
99+
return self.nameTextField.stringValue.length && self.passwordTextField.stringValue.length;
100+
}
101+
102+
#pragma mark - Repository
103+
- (void)repository:(GCRepository*)repository willStartTransferWithURL:(NSURL*)url {
104+
[self.model willStartTransfer];
105+
}
106+
107+
- (BOOL)repository:(GCRepository*)repository requiresPlainTextAuthenticationForURL:(NSURL*)url user:(NSString*)user username:(NSString**)username password:(NSString**)password {
108+
if (self.model.useKeychain) {
109+
[self.model unsetUseKeychain];
110+
if ([KeychainAccessor loadPlainTextAuthenticationFormKeychainForURL:url user:user username:username password:password allowInteraction:YES]) {
111+
return YES;
112+
}
113+
} else {
114+
XLOG_VERBOSE(@"Skipping Keychain lookup for repeated authentication failures");
115+
}
116+
117+
// TODO: Add data to model and when window is appearing, we should set data from model.
118+
// We need two callbacks ( willPresentModal and didPresentModal ).
119+
self.model.url = url;
120+
121+
self.model.name = *username ? *username : @"";
122+
self.model.password = @"";
123+
124+
if (self.windowLoaded) {
125+
[self beforeRunInModal];
126+
}
127+
else {
128+
// look at -windowDidLoad when window first time loaded.
129+
}
130+
131+
if ([NSApp runModalForWindow:self.window] && self.credentialsExists) {
132+
self.model.name = self.nameTextField.stringValue;
133+
self.model.password = self.passwordTextField.stringValue;
134+
*username = self.model.name;
135+
*password = self.model.password;
136+
return YES;
137+
}
138+
139+
return NO;
140+
}
141+
142+
- (void)repository:(GCRepository*)repository didFinishTransferWithURL:(NSURL*)url success:(BOOL)success {
143+
[self.model didFinishTransferWithURL:url success:success onResult:^(AuthenticationWindowControllerModel *model) {
144+
if (model) {
145+
[KeychainAccessor savePlainTextAuthenticationToKeychainForURL:url username:model.name password:model.password];
146+
}
147+
}];
148+
}
149+
150+
@end

0 commit comments

Comments
 (0)