iOS web browser with a focus on security and privacy
at master 241 lines 6.5 kB view raw
1/* 2 * Endless 3 * Copyright (c) 2014-2015 joshua stein <jcs@jcs.org> 4 * 5 * See LICENSE file for redistribution terms. 6 */ 7 8#import "AppDelegate.h" 9#import "HSTSCache.h" 10#import "NSString+IPAddress.h" 11 12/* rfc6797 HTTP Strict Transport Security */ 13 14/* note that UIWebView has its own HSTS cache that comes preloaded with a big plist of hosts, but we can't change it or manually add to it */ 15 16@implementation HSTSCache { 17 AppDelegate *appDelegate; 18} 19static NSDictionary *_preloadedHosts; 20 21+ (NSString *)hstsCachePath 22{ 23 NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]; 24 return [path stringByAppendingPathComponent:@"hsts_cache.plist"]; 25} 26 27- (HSTSCache *)init 28{ 29 self = [super init]; 30 31 _dict = [[NSMutableDictionary alloc] init]; 32 appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; 33 34 return self; 35} 36 37+ (HSTSCache *)retrieve 38{ 39 HSTSCache *hc = [[HSTSCache alloc] init]; 40 NSFileManager *fileManager = [NSFileManager defaultManager]; 41 if ([fileManager fileExistsAtPath:[[self class] hstsCachePath]]) { 42 hc.dict = [NSMutableDictionary dictionaryWithContentsOfFile:[[self class] hstsCachePath]]; 43 } 44 else { 45 hc.dict = [[NSMutableDictionary alloc] initWithCapacity:50]; 46 } 47 48 /* mix in preloaded */ 49 NSString *path = [[NSBundle mainBundle] pathForResource:@"hsts_preload" ofType:@"plist"]; 50 if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { 51 NSDictionary *tmp = [NSDictionary dictionaryWithContentsOfFile:path]; 52 for (NSString *host in [tmp allKeys]) { 53 NSDictionary *hostdef = [tmp objectForKey:host]; 54 NSMutableDictionary *v = [[NSMutableDictionary alloc] init]; 55 56 [v setObject:[NSDate dateWithTimeIntervalSinceNow:(60 * 60 * 24 * 365)] forKey:HSTS_KEY_EXPIRATION]; 57 [v setObject:@YES forKey:HSTS_KEY_PRELOADED]; 58 59 NSNumber *is = [hostdef objectForKey:@"include_subdomains"]; 60 if ([is intValue] == 1) { 61 [v setObject:@YES forKey:HSTS_KEY_ALLOW_SUBDOMAINS]; 62 } 63 64 [[hc dict] setObject:v forKey:host]; 65 } 66 67#ifdef TRACE_HSTS 68 NSLog(@"[HSTSCache] locked and loaded with %lu preloaded hosts", [tmp count]); 69#endif 70 } 71 else { 72 NSLog(@"[HSTSCache] no preload plist at %@", path); 73 } 74 75 return hc; 76} 77 78- (void)persist 79{ 80 @try { 81 [self writeToFile:[[self class] hstsCachePath] atomically:YES]; 82 } 83 @catch(NSException *e) { 84 NSLog(@"[HSTSCache] failed persisting to file: %@", e); 85 } 86} 87 88- (NSURL *)rewrittenURI:(NSURL *)URL 89{ 90 if (![[URL scheme] isEqualToString:@"http"]) { 91 return URL; 92 } 93 94 NSString *host = [[URL host] lowercaseString]; 95 NSString *matchHost = [host copy]; 96 97 /* 8.3: ignore when host is a bare ip address */ 98 if ([host isValidIPAddress]) { 99 return URL; 100 } 101 102 NSDictionary *params = [self objectForKey:host]; 103 if (params == nil) { 104 /* for a host of x.y.z.example.com, try y.z.example.com, z.example.com, example.com, etc. */ 105 NSArray *hostp = [host componentsSeparatedByString:@"."]; 106 for (int i = 1; i < [hostp count]; i++) { 107 NSString *wc = [[hostp subarrayWithRange:NSMakeRange(i, [hostp count] - i)] componentsJoinedByString:@"."]; 108 109 if (((params = [self objectForKey:wc]) != nil) && [params objectForKey:HSTS_KEY_ALLOW_SUBDOMAINS]) { 110 matchHost = wc; 111 break; 112 } 113 } 114 } 115 116 if (params != nil) { 117 NSDate *exp = [params objectForKey:HSTS_KEY_EXPIRATION]; 118 if ([exp timeIntervalSince1970] < [[NSDate date] timeIntervalSince1970]) { 119#ifdef TRACE_HSTS 120 NSLog(@"[HSTSCache] entry for %@ expired at %@", matchHost, exp); 121#endif 122 [self removeObjectForKey:matchHost]; 123 params = nil; 124 } 125 } 126 127 if (params == nil) { 128 return URL; 129 } 130 131 NSURLComponents *URLc = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO]; 132 133 [URLc setScheme:@"https"]; 134 135 /* 8.3.5: nullify port unless it's a non-standard one */ 136 if ([URLc port] != nil && [[URLc port] intValue] == 80) { 137 [URLc setPort:nil]; 138 } 139 140#ifdef TRACE_HSTS 141 NSLog(@"[HSTSCache] %@rewrote %@ to %@", ([params objectForKey:HSTS_KEY_PRELOADED] ? @"[preloaded] " : @""), URL, [URLc URL]); 142#endif 143 144 return [URLc URL]; 145} 146 147- (void)parseHSTSHeader:(NSString *)header forHost:(NSString *)host 148{ 149 NSMutableDictionary *params = [[NSMutableDictionary alloc] initWithCapacity:3]; 150 host = [host lowercaseString]; 151 152 /* 8.1.1: reject caching when host is a bare ip address */ 153 if ([host isValidIPAddress]) 154 return; 155 156#ifdef TRACE_HSTS 157 NSLog(@"[HSTSCache] [%@] %@", host, header); 158#endif 159 160 NSArray *kvs = [header componentsSeparatedByString:@";"]; 161 for (NSString *kv in kvs) { 162 NSArray *kvparts = [kv componentsSeparatedByString:@"="]; 163 NSString *key, *value; 164 165 key = [kvparts[0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 166 167 if ([kvparts count] > 1) { 168 value = [[kvparts[1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; 169 } 170 171 if ([[key lowercaseString] isEqualToString:@"max-age"]) { 172 long long age = [value longLongValue]; 173 174 if (age == 0) { 175#ifdef TRACE_HSTS 176 NSLog(@"[HSTSCache] [%@] got max-age=0, deleting", host); 177#endif 178 /* TODO: if a preloaded entry exists, cache a negative entry */ 179 [self removeObjectForKey:host]; 180 return; 181 } 182 else { 183 NSDate *expire = [[NSDate date] dateByAddingTimeInterval:age]; 184 [params setObject:expire forKey:HSTS_KEY_EXPIRATION]; 185 } 186 } 187 else if ([[key lowercaseString] isEqualToString:@"includesubdomains"]) { 188 [params setObject:@YES forKey:HSTS_KEY_ALLOW_SUBDOMAINS]; 189 } 190 else if ([[key lowercaseString] isEqualToString:@"preload"] || 191 [[key lowercaseString] isEqualToString:@""]) { 192 /* ignore */ 193 } 194 else { 195#ifdef TRACE_HSTS 196 NSLog(@"[HSTSCache] [%@] unknown parameter \"%@\"", host, key); 197#endif 198 } 199 } 200 201 if ([params objectForKey:HSTS_KEY_EXPIRATION]) { 202 [self setValue:params forKey:host]; 203 } 204} 205 206/* NSMutableDictionary composition pass-throughs */ 207 208- (id)objectForKey:(id)aKey 209{ 210 return [[self dict] objectForKey:aKey]; 211} 212 213- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile 214{ 215 @try { 216 return [[self dict] writeToFile:path atomically:useAuxiliaryFile]; 217 } 218 @catch(NSException *e) { 219 NSLog(@"[HSTSCache] failed persisting to file: %@", e); 220 } 221 222 return false; 223} 224 225- (void)setValue:(id)value forKey:(NSString *)key 226{ 227 if (value != nil && key != nil) 228 [[self dict] setValue:value forKey:key]; 229} 230 231- (void)removeObjectForKey:(id)aKey 232{ 233 [[self dict] removeObjectForKey:aKey]; 234} 235 236- (NSArray *)allKeys 237{ 238 return [[self dict] allKeys]; 239} 240 241@end