Skip to content

Commit

Permalink
Authenticate requests between components
Browse files Browse the repository at this point in the history
Use a randomly generated token to authenticate requests to the server for all
endpoints, including WebSockets.  The token is passed to the node server via
STDIN, which should sufficiently prevent eavesdropping.
  • Loading branch information
execjosh committed Aug 21, 2022
1 parent b3f370f commit 9c7cf76
Show file tree
Hide file tree
Showing 21 changed files with 252 additions and 37 deletions.
1 change: 1 addition & 0 deletions Uebersicht/UBAppDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

- (void)widgetDirDidChange;
- (void)interactionDidChange;
- (void)enableSecurityDidChange;
- (void)screensChanged:(NSDictionary*)screens;
- (IBAction)showPreferences:(id)sender;
- (IBAction)openWidgetDir:(id)sender;
Expand Down
111 changes: 91 additions & 20 deletions Uebersicht/UBAppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ @implementation UBAppDelegate {
UBWidgetsStore* widgetsStore;
UBWidgetsController* widgetsController;
BOOL needsRefresh;
NSString *token;
}

@synthesize statusBarMenu;

static const uint kTokenLength256Bits = 32;

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
needsRefresh = YES;
Expand Down Expand Up @@ -110,33 +113,58 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
[self listenToWallpaperChanges];
}

- (NSDictionary*)fetchState
- (void)fetchState:(void (^)(NSDictionary*))callback
{
[[UBWebSocket sharedSocket] open:[self serverUrl:@"ws"]];
[[UBWebSocket sharedSocket] open:[self serverUrl:@"ws"]
token:token];

NSURL *urlPath = [[self serverUrl:@"http"] URLByAppendingPathComponent: @"state/"];
NSData *jsonData = [NSData dataWithContentsOfURL:urlPath];
NSError *error = nil;
NSDictionary *dataDictionary = [NSJSONSerialization
JSONObjectWithData: jsonData
options: NSJSONReadingMutableContainers
error: &error
];
if (error) NSLog(@"%@", error);
return dataDictionary;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:urlPath];
[request setValue:@"Übersicht" forHTTPHeaderField:@"Origin"];
[request setValue:[NSString stringWithFormat:@"token=%@", token] forHTTPHeaderField:@"Cookie"];
NSURLSessionDataTask *t = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(@"error loading state: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{
callback(@{});
});
return;
}

NSError *jsonError = nil;
NSDictionary *dataDictionary = [NSJSONSerialization
JSONObjectWithData: data
options: NSJSONReadingMutableContainers
error: &jsonError
];
if (jsonError) {
NSLog(@"error parsing state: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{
callback(@{});
});
return;
}

dispatch_async(dispatch_get_main_queue(), ^{
callback(dataDictionary);
});
}];
[t resume];
}

- (void)startUp
{

NSLog(@"starting server task");

void (^handleData)(NSString*) = ^(NSString* output) {
// note that these might be called several times
if ([output rangeOfString:@"server started"].location != NSNotFound) {
[self->widgetsStore reset: [self fetchState]];
// this will trigger a render
[self->screensController syncScreens:self];

[self fetchState:^(NSDictionary* state) {
[self->widgetsStore reset: state];
// this will trigger a render
[self->screensController syncScreens:self];
}];
} else if ([output rangeOfString:@"EADDRINUSE"].location != NSNotFound) {
self->portOffset++;
}
Expand Down Expand Up @@ -216,15 +244,28 @@ - (NSTask*)launchWidgetServer:(NSString*)widgetPath
onData:(void (^)(NSString*))dataHandler
onExit:(void (^)(NSTask*))exitHandler
{
token = generateToken(kTokenLength256Bits);

NSBundle* bundle = [NSBundle mainBundle];
NSString* nodePath = [bundle pathForResource:@"localnode" ofType:nil];
NSString* serverPath = [bundle pathForResource:@"server" ofType:@"js"];
BOOL loginShell = [[NSUserDefaults standardUserDefaults]
boolForKey:@"loginShell"
];

NSTask *task = [[NSTask alloc] init];

NSPipe *inPipe = [NSPipe pipe];
NSFileHandle *fh = [inPipe fileHandleForWriting];
NSMutableDictionary *secrets = [[NSMutableDictionary alloc] init];
secrets[@"token"] = token;
NSError *jsonErr = NULL;
NSData *stdinData = [NSJSONSerialization dataWithJSONObject:secrets options:0 error:&jsonErr];
if (!stdinData) {
NSLog(@"[FATAL] %@", jsonErr);
return NULL;
}
[fh writeData:stdinData];
[fh closeFile];
[task setStandardInput:inPipe];

[task setStandardOutput:[NSPipe pipe]];
[task.standardOutput fileHandleForReading].readabilityHandler = ^(NSFileHandle *handle) {
NSData *output = [handle availableData];
Expand Down Expand Up @@ -252,7 +293,8 @@ - (NSTask*)launchWidgetServer:(NSString*)widgetPath
@"-d", widgetPath,
@"-p", [NSString stringWithFormat:@"%d", PORT + portOffset],
@"-s", [[self getPreferencesDir] path],
loginShell ? @"--login-shell" : @""
!preferences.enableSecurity ? @"--disable-token" : @"",
preferences.loginShell ? @"--login-shell" : @""
]];

[task launch];
Expand Down Expand Up @@ -294,6 +336,7 @@ - (void)screensChanged:(NSDictionary*)screens
[windowsController
updateWindows:screens
baseUrl: [self serverUrl: @"http"]
token:token
interactionEnabled: preferences.enableInteraction
forceRefresh: needsRefresh
];
Expand Down Expand Up @@ -323,6 +366,11 @@ - (void)interactionDidChange
[screensController syncScreens:self];
}

- (void)enableSecurityDidChange
{
[self shutdown:true];
}

- (IBAction)showPreferences:(id)sender
{
[preferences showWindow:nil];
Expand Down Expand Up @@ -453,6 +501,29 @@ void wallpaperSettingsChanged(
}
}

/*!
@function generateToken
@abstract
Returns a base64-encoded @p NSString* of specified number of random bytes.
@param length
A reasonably large, non-zero number representing the length in bytes.
For example, a value of 32 would generate a 256-bit token.
@result Returns @p NSString* on success; panics on failure.
*/
NSString* generateToken(uint length) {
UInt8 buf[length];

int error = SecRandomCopyBytes(kSecRandomDefault, length, &buf);
if (error != errSecSuccess) {
panic("failed to generate token");
}

return [[NSData dataWithBytes:buf length:length] base64EncodedStringWithOptions:0];
}

#
# pragma mark script support
#
Expand Down
1 change: 1 addition & 0 deletions Uebersicht/UBPreferencesController.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
@property NSURL* widgetDir;
@property BOOL loginShell;
@property BOOL enableInteraction;
@property BOOL enableSecurity;

- (IBAction)showFilePicker:(id)sender;

Expand Down
22 changes: 21 additions & 1 deletion Uebersicht/UBPreferencesController.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ @implementation UBPreferencesController {
LSSharedFileListRef loginItems;
}

static NSString * const kDefaultsEnableSecurity = @"enableSecurity";

@synthesize filePicker;

- (id)initWithWindowNibName:(NSString *)windowNibName
Expand All @@ -26,7 +28,8 @@ - (id)initWithWindowNibName:(NSString *)windowNibName
NSData* defaultWidgetDir = [self ensureDefaultsWidgetDir];
NSDictionary *appDefaults = @{
@"widgetDirectory": defaultWidgetDir,
@"enableInteraction": @YES
@"enableInteraction": @YES,
kDefaultsEnableSecurity: @YES,
};
[[NSUserDefaults standardUserDefaults] registerDefaults:appDefaults];

Expand Down Expand Up @@ -196,6 +199,23 @@ - (void)setEnableInteraction:(BOOL)enabled
[(UBAppDelegate *)[NSApp delegate] interactionDidChange];
}

#
#pragma mark Security
#

- (BOOL)enableSecurity
{
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
return [defaults boolForKey:kDefaultsEnableSecurity];
}

- (void)setEnableSecurity:(BOOL)enabled
{
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:enabled forKey:kDefaultsEnableSecurity];
[(UBAppDelegate *)[NSApp delegate] enableSecurityDidChange];
}

#
#pragma mark Startup
#
Expand Down
37 changes: 33 additions & 4 deletions Uebersicht/UBPreferencesController.xib
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
<window title="Preferences" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="1">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="472" y="636" width="580" height="236"/>
<rect key="contentRect" x="472" y="636" width="580" height="300"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<value key="minSize" type="size" width="580" height="236"/>
<value key="maxSize" type="size" width="580" height="236"/>
<value key="minSize" type="size" width="580" height="300"/>
<value key="maxSize" type="size" width="580" height="300"/>
<view key="contentView" autoresizesSubviews="NO" id="2">
<rect key="frame" x="0.0" y="0.0" width="580" height="236"/>
<rect key="frame" x="0.0" y="0.0" width="580" height="300"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5">
Expand Down Expand Up @@ -124,6 +124,15 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" preferredMaxLayoutWidth="268" translatesAutoresizingMaskIntoConstraints="NO" id="BRa-2G-OzE">
<rect key="frame" x="239" y="217" width="330" height="42"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" sendsActionOnEndEditing="YES" title="CAUTION! Disabling this will allow any local user or process to execute commands under your account." id="W8m-Vx-Tdo">
<font key="font" metaFont="label" size="11"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" preferredMaxLayoutWidth="268" translatesAutoresizingMaskIntoConstraints="NO" id="VVJ-Cl-FSt">
<rect key="frame" x="239" y="88" width="312" height="42"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
Expand All @@ -133,6 +142,26 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="4Cm-6a-MW9">
<rect key="frame" x="220" y="265" width="182" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" title="Enable security measures" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="6iu-kH-BSa">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="-2" name="value" keyPath="enableSecurity" id="OTS-dV-otR"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="HZc-Ku-Lxb">
<rect key="frame" x="158" y="267" width="58" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Security:" id="7yt-3i-Laz">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
</view>
<connections>
Expand Down
3 changes: 2 additions & 1 deletion Uebersicht/UBWebSocket.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
@interface UBWebSocket : NSObject <SRWebSocketDelegate>

+ (id)sharedSocket;
- (void)open:(NSURL*)aUrl;
- (void)open:(NSURL*)aUrl
token:(NSString*)aToken;
- (void)close;
- (void)send:(id)message;
- (void)listen:(void (^)(id))listener;
Expand Down
14 changes: 13 additions & 1 deletion Uebersicht/UBWebSocket.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ @implementation UBWebSocket {
NSMutableArray* queuedMessages;
SRWebSocket* ws;
NSURL* url;
NSString *token;
}


Expand Down Expand Up @@ -49,16 +50,26 @@ - (void)listen:(void (^)(id))listener
}

- (void)open:(NSURL*)aUrl
token:(NSString*)aToken
{
if (ws) {
return;
}

token = aToken;
url = aUrl;
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
[request setValue:@"Übersicht" forHTTPHeaderField:@"Origin"];
ws = [[SRWebSocket alloc] initWithURLRequest: request];
ws.delegate = self;
ws.requestCookies = @[
[NSHTTPCookie cookieWithProperties:@{
NSHTTPCookieDomain: url.host,
NSHTTPCookiePath: @"/",
NSHTTPCookieName: @"token",
NSHTTPCookieValue: token,
}],
];
[ws open];
}

Expand All @@ -76,7 +87,8 @@ - (void)reopen
{
[self close];
if (url) {
[self open:url];
[self open:url
token:token];
}
}

Expand Down
1 change: 1 addition & 0 deletions Uebersicht/UBWebViewController.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

- (id)initWithFrame:(NSRect)frame;
- (void)load:(NSURL*)url;
- (void)setToken:(NSString*)token;
- (void)reload;
- (void)redraw;
- (void)destroy;
Expand Down
Loading

0 comments on commit 9c7cf76

Please sign in to comment.