iOS web browser with a focus on security and privacy
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