1/*****************************************************************************
2 * keychain.m: Darwin Keychain keystore module
3 *****************************************************************************
4 * Copyright © 2016, 2018 VLC authors, VideoLAN and VideoLabs
5 *
6 * Author: Felix Paul Kühne <fkuehne # videolabs.io>
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU Lesser General Public License as published by
10 * the Free Software Foundation; either version 2.1 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 * GNU Lesser General Public License for more details.
17 *
18 * You should have received a copy of the GNU Lesser General Public License
19 * along with this program; if not, write to the Free Software Foundation,
20 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
21 *****************************************************************************/
22
23#ifdef HAVE_CONFIG_H
24# include "config.h"
25#endif
26
27#include <vlc_common.h>
28#include <vlc_plugin.h>
29#include <vlc_keystore.h>
30
31#include "list_util.h"
32
33#import <Cocoa/Cocoa.h>
34#import <Security/Security.h>
35
36static int Open(vlc_object_t *);
37
38static const int sync_list[] =
39{ 0, 1, 2 };
40static const char *const sync_list_text[] = {
41    N_("Yes"), N_("No"), N_("Any")
42};
43
44static const int accessibility_list[] =
45{ 0, 1, 2, 3, 4, 5, 6, 7 };
46static const char *const accessibility_list_text[] = {
47    N_("System default"),
48    N_("After first unlock"),
49    N_("After first unlock, on this device only"),
50    N_("Always"),
51    N_("When passcode set, on this device only"),
52    N_("Always, on this device only"),
53    N_("When unlocked"),
54    N_("When unlocked, on this device only")
55};
56
57#define SYNC_ITEMS_TEXT N_("Synchronize stored items")
58#define SYNC_ITEMS_LONGTEXT N_("Synchronizes stored items via iCloud Keychain if enabled in the user domain.")
59
60#define ACCESSIBILITY_TYPE_TEXT N_("Accessibility type for all future passwords saved to the Keychain")
61
62#define ACCESS_GROUP_TEXT N_("Keychain access group")
63#define ACCESS_GROUP_LONGTEXT N_("Keychain access group as defined by the app entitlements.")
64
65/* VLC can be compiled against older SDKs (like before OS X 10.10)
66 * but newer features should still be available.
67 * Hence, re-define things as needed */
68#ifndef kSecAttrSynchronizable
69#define kSecAttrSynchronizable CFSTR("sync")
70#endif
71
72#ifndef kSecAttrSynchronizableAny
73#define kSecAttrSynchronizableAny CFSTR("syna")
74#endif
75
76#ifndef kSecAttrAccessGroup
77#define kSecAttrAccessGroup CFSTR("agrp")
78#endif
79
80#ifndef kSecAttrAccessibleAfterFirstUnlock
81#define kSecAttrAccessibleAfterFirstUnlock CFSTR("ck")
82#endif
83
84#ifndef kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
85#define kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly CFSTR("cku")
86#endif
87
88#ifndef kSecAttrAccessibleAlways
89#define kSecAttrAccessibleAlways CFSTR("dk")
90#endif
91
92#ifndef kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
93#define kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly CFSTR("akpu")
94#endif
95
96#ifndef kSecAttrAccessibleAlwaysThisDeviceOnly
97#define kSecAttrAccessibleAlwaysThisDeviceOnly CFSTR("dku")
98#endif
99
100#ifndef kSecAttrAccessibleWhenUnlocked
101#define kSecAttrAccessibleWhenUnlocked CFSTR("ak")
102#endif
103
104#ifndef kSecAttrAccessibleWhenUnlockedThisDeviceOnly
105#define kSecAttrAccessibleWhenUnlockedThisDeviceOnly CFSTR("aku")
106#endif
107
108vlc_module_begin()
109    set_shortname(N_("Keychain keystore"))
110    set_description(N_("Keystore for iOS, Mac OS X and tvOS"))
111    set_category(CAT_ADVANCED)
112    set_subcategory(SUBCAT_ADVANCED_MISC)
113    add_integer("keychain-synchronize", 1, SYNC_ITEMS_TEXT, SYNC_ITEMS_LONGTEXT, true)
114    change_integer_list(sync_list, sync_list_text)
115    add_integer("keychain-accessibility-type", 0, ACCESSIBILITY_TYPE_TEXT, ACCESSIBILITY_TYPE_TEXT, true)
116    change_integer_list(accessibility_list, accessibility_list_text)
117    add_string("keychain-access-group", NULL, ACCESS_GROUP_TEXT, ACCESS_GROUP_LONGTEXT, true)
118    set_capability("keystore", 100)
119    set_callbacks(Open, NULL)
120vlc_module_end ()
121
122static NSMutableDictionary * CreateQuery(vlc_keystore *p_keystore)
123{
124    NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:3];
125    [dictionary setObject:(__bridge id)kSecClassInternetPassword forKey:(__bridge id)kSecClass];
126
127    [dictionary setObject:@"VLC-Password-Service" forKey:(__bridge id)kSecAttrService];
128
129    const char * psz_access_group = var_InheritString(p_keystore, "keychain-access-group");
130    if (psz_access_group) {
131        [dictionary setObject:[NSString stringWithUTF8String:psz_access_group] forKey:(__bridge id)kSecAttrAccessGroup];
132    }
133
134    id syncValue;
135    int syncMode = var_InheritInteger(p_keystore, "keychain-synchronize");
136
137    if (syncMode == 2) {
138        syncValue = (__bridge id)kSecAttrSynchronizableAny;
139    } else if (syncMode == 0) {
140        syncValue = @(YES);
141    } else {
142        syncValue = @(NO);
143    }
144
145    [dictionary setObject:syncValue forKey:(__bridge id)(kSecAttrSynchronizable)];
146
147    return dictionary;
148}
149
150static NSString * ErrorForStatus(OSStatus status)
151{
152    NSString *message = nil;
153
154    switch (status) {
155#if TARGET_OS_IPHONE
156        case errSecUnimplemented: {
157            message = @"Query unimplemented";
158            break;
159        }
160        case errSecParam: {
161            message = @"Faulty parameter";
162            break;
163        }
164        case errSecAllocate: {
165            message = @"Allocation failure";
166            break;
167        }
168        case errSecNotAvailable: {
169            message = @"Query not available";
170            break;
171        }
172        case errSecDuplicateItem: {
173            message = @"Duplicated item";
174            break;
175        }
176        case errSecItemNotFound: {
177            message = @"Item not found";
178            break;
179        }
180        case errSecInteractionNotAllowed: {
181            message = @"Interaction not allowed";
182            break;
183        }
184        case errSecDecode: {
185            message = @"Decoding failure";
186            break;
187        }
188        case errSecAuthFailed: {
189            message = @"Authentication failure";
190            break;
191        }
192        case -34018: {
193            message = @"iCloud Keychain failure";
194            break;
195        }
196        default: {
197            message = @"Unknown generic error";
198        }
199#else
200        default:
201            message = (__bridge_transfer NSString *)SecCopyErrorMessageString(status, NULL);
202#endif
203    }
204
205    return message;
206}
207
208#define OSX_MAVERICKS (NSAppKitVersionNumber >= 1265)
209extern const CFStringRef kSecAttrAccessible;
210
211#pragma clang diagnostic push
212#pragma clang diagnostic ignored "-Wpartial-availability"
213static void SetAccessibilityForQuery(vlc_keystore *p_keystore,
214                                     NSMutableDictionary *query)
215{
216    if (!OSX_MAVERICKS)
217	return;
218
219    int accessibilityType = var_InheritInteger(p_keystore, "keychain-accessibility-type");
220    switch (accessibilityType) {
221        case 1:
222            [query setObject:(__bridge id)kSecAttrAccessibleAfterFirstUnlock forKey:(__bridge id)kSecAttrAccessible];
223            break;
224        case 2:
225            [query setObject:(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
226            break;
227        case 3:
228            [query setObject:(__bridge id)kSecAttrAccessibleAlways forKey:(__bridge id)kSecAttrAccessible];
229            break;
230        case 4:
231            [query setObject:(__bridge id)kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
232            break;
233        case 5:
234            [query setObject:(__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
235            break;
236        case 6:
237            [query setObject:(__bridge id)kSecAttrAccessibleWhenUnlocked forKey:(__bridge id)kSecAttrAccessible];
238            break;
239        case 7:
240            [query setObject:(__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
241            break;
242        default:
243            break;
244    }
245}
246#pragma clang diagnostic pop
247
248static void SetAttributesForQuery(const char *const ppsz_values[KEY_MAX], NSMutableDictionary *query, const char *psz_label)
249{
250    const char *psz_protocol = ppsz_values[KEY_PROTOCOL];
251    const char *psz_user = ppsz_values[KEY_USER];
252    const char *psz_server = ppsz_values[KEY_SERVER];
253    const char *psz_path = ppsz_values[KEY_PATH];
254    const char *psz_port = ppsz_values[KEY_PORT];
255
256    if (psz_label) {
257        [query setObject:[NSString stringWithUTF8String:psz_label] forKey:(__bridge id)kSecAttrLabel];
258    }
259    if (psz_protocol) {
260        [query setObject:[NSString stringWithUTF8String:psz_protocol] forKey:(__bridge id)kSecAttrProtocol];
261    }
262    if (psz_user) {
263        [query setObject:[NSString stringWithUTF8String:psz_user] forKey:(__bridge id)kSecAttrAccount];
264    }
265    if (psz_server) {
266        [query setObject:[NSString stringWithUTF8String:psz_server] forKey:(__bridge id)kSecAttrServer];
267    }
268    if (psz_path) {
269        [query setObject:[NSString stringWithUTF8String:psz_path] forKey:(__bridge id)kSecAttrPath];
270    }
271    if (psz_port) {
272        [query setObject:[NSNumber numberWithInt:atoi(psz_port)] forKey:(__bridge id)kSecAttrPort];
273    }
274}
275
276static int Store(vlc_keystore *p_keystore,
277                 const char *const ppsz_values[KEY_MAX],
278                 const uint8_t *p_secret,
279                 size_t i_secret_len,
280                 const char *psz_label)
281{
282    OSStatus status;
283
284    if (!ppsz_values[KEY_PROTOCOL] || !p_secret) {
285        return VLC_EGENERIC;
286    }
287
288    NSMutableDictionary *query = nil;
289    NSMutableDictionary *searchQuery = CreateQuery(p_keystore);
290
291    /* set attributes */
292    SetAttributesForQuery(ppsz_values, searchQuery, psz_label);
293
294    // One return type must be added for SecItemCopyMatching, even if not used.
295    // Older macOS versions (10.7) are very picky here...
296    [searchQuery setObject:@(YES) forKey:(__bridge id)kSecReturnRef];
297    CFTypeRef result = NULL;
298
299    /* search */
300    status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, &result);
301    /* create storage unit */
302    NSData *secretData = [[NSString stringWithFormat:@"%s", p_secret] dataUsingEncoding:NSUTF8StringEncoding];
303
304    if (status == errSecSuccess) {
305        msg_Dbg(p_keystore, "the item was already known to keychain, so it will be updated");
306        /* item already existed in keychain, let's update */
307        query = [[NSMutableDictionary alloc] init];
308
309        /* just set the secret data */
310        [query setObject:secretData forKey:(__bridge id)kSecValueData];
311
312        status = SecItemUpdate((__bridge CFDictionaryRef)(searchQuery), (__bridge CFDictionaryRef)(query));
313    } else if (status == errSecItemNotFound) {
314        msg_Dbg(p_keystore, "creating new item in keychain");
315        /* item not found, let's create! */
316        query = CreateQuery(p_keystore);
317
318        /* set attributes */
319        SetAttributesForQuery(ppsz_values, query, psz_label);
320
321        /* set accessibility */
322        SetAccessibilityForQuery(p_keystore, query);
323
324        /* set secret data */
325        [query setObject:secretData forKey:(__bridge id)kSecValueData];
326
327        status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
328    }
329    if (status != errSecSuccess) {
330        msg_Err(p_keystore, "Storage failed (%i: '%s')", status, [ErrorForStatus(status) UTF8String]);
331        return VLC_EGENERIC;
332    }
333
334    return VLC_SUCCESS;
335}
336
337static unsigned int Find(vlc_keystore *p_keystore,
338                         const char *const ppsz_values[KEY_MAX],
339                         vlc_keystore_entry **pp_entries)
340{
341    CFTypeRef result = NULL;
342
343    NSMutableDictionary *query = CreateQuery(p_keystore);
344    [query setObject:@(YES) forKey:(__bridge id)kSecReturnRef];
345    [query setObject:(__bridge id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit];
346
347    /* set attributes */
348    SetAttributesForQuery(ppsz_values, query, NULL);
349
350    /* search */
351    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
352
353    if (status != errSecSuccess) {
354        msg_Warn(p_keystore, "lookup failed (%i: '%s')", status, [ErrorForStatus(status) UTF8String]);
355        return 0;
356    }
357
358    NSArray *listOfResults = (__bridge_transfer NSArray *)result;
359
360    NSUInteger count = listOfResults.count;
361    msg_Dbg(p_keystore, "found %lu result(s) for the provided attributes", count);
362
363    vlc_keystore_entry *p_entries = calloc(count,
364                                           sizeof(vlc_keystore_entry));
365    if (!p_entries)
366        return 0;
367
368    for (NSUInteger i = 0; i < count; i++) {
369        vlc_keystore_entry *p_entry = &p_entries[i];
370        if (ks_values_copy((const char **)p_entry->ppsz_values, ppsz_values) != VLC_SUCCESS) {
371            vlc_keystore_release_entries(p_entries, 1);
372            return 0;
373        }
374
375        SecKeychainItemRef itemRef = (__bridge SecKeychainItemRef)([listOfResults objectAtIndex:i]);
376
377        SecKeychainAttributeInfo attrInfo;
378        attrInfo.count = 1;
379        UInt32 tags[1] = {kSecAccountItemAttr}; //, kSecAccountItemAttr, kSecServerItemAttr, kSecPortItemAttr, kSecProtocolItemAttr, kSecPathItemAttr};
380        attrInfo.tag = tags;
381        attrInfo.format = NULL;
382
383        SecKeychainAttributeList *attrList = NULL;
384
385        UInt32 secretDataLength;
386        void * secretData;
387
388        status = SecKeychainItemCopyAttributesAndData(itemRef, &attrInfo, NULL, &attrList, &secretDataLength, &secretData);
389
390        if (status != noErr) {
391            msg_Err(p_keystore, "Lookup error: %i (%s)", status, [ErrorForStatus(status) UTF8String]);
392            vlc_keystore_release_entries(p_entries, count);
393            return 0;
394        }
395
396        for (unsigned x = 0; x < attrList->count; x++) {
397            SecKeychainAttribute *attr = &attrList->attr[i];
398            switch (attr->tag) {
399                case kSecAccountItemAttr:
400                    if (!p_entry->ppsz_values[KEY_USER]) {
401                        msg_Dbg(p_keystore, "using account name from the keychain for login");
402
403                        char *paddedName = calloc(1, attr->length + 1);
404                        memcpy(paddedName, attr->data, attr->length);
405                        p_entry->ppsz_values[KEY_USER] = paddedName;
406                    }
407                    break;
408                default:
409                    break;
410            }
411        }
412
413        /* we need to do some padding here, as string is expected to be 0 terminated */
414        uint8_t *paddedSecretData = calloc(1, secretDataLength + 1);
415        memcpy(paddedSecretData, secretData, secretDataLength);
416        vlc_keystore_entry_set_secret(p_entry, paddedSecretData, secretDataLength + 1);
417        free(paddedSecretData);
418
419        SecKeychainItemFreeAttributesAndData(attrList, secretData);
420    }
421
422    *pp_entries = p_entries;
423
424    return count;
425}
426
427static unsigned int Remove(vlc_keystore *p_keystore,
428                           const char *const ppsz_values[KEY_MAX])
429{
430    OSStatus status;
431
432    NSMutableDictionary *query = CreateQuery(p_keystore);
433
434    SetAttributesForQuery(ppsz_values, query, NULL);
435
436    CFTypeRef result = NULL;
437    [query setObject:@(YES) forKey:(__bridge id)kSecReturnRef];
438    [query setObject:(__bridge id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit];
439
440    BOOL failed = NO;
441    status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
442
443    NSUInteger matchCount = 0;
444
445    if (status == errSecSuccess) {
446        NSArray *matches = (__bridge_transfer NSArray *)result;
447        matchCount = matches.count;
448        msg_Dbg(p_keystore, "Found %lu match(es) for deletion", matchCount);
449
450        for (NSUInteger x = 0; x < matchCount; x++) {
451            status = SecKeychainItemDelete((__bridge SecKeychainItemRef _Nonnull)([matches objectAtIndex:x]));
452            if (status != noErr) {
453                msg_Err(p_keystore, "Deletion error %i (%s)", status , [ErrorForStatus(status) UTF8String]);
454                failed = YES;
455            }
456        }
457    } else {
458        msg_Err(p_keystore, "Lookup error for deletion %i (%s)", status, [ErrorForStatus(status) UTF8String]);
459        return VLC_EGENERIC;
460    }
461
462    if (failed)
463        return VLC_EGENERIC;
464
465    return matchCount;
466}
467
468static int Open(vlc_object_t *p_this)
469{
470    vlc_keystore *p_keystore = (vlc_keystore *)p_this;
471
472    p_keystore->p_sys = NULL;
473    p_keystore->pf_store = Store;
474    p_keystore->pf_find = Find;
475    p_keystore->pf_remove = Remove;
476
477    return VLC_SUCCESS;
478}
479