contrib/gpg-error: simplify darwin triplet handling
[vlc.git] / modules / keystore / keychain.m
blobf7fa1532bb82d73ce98242fad7b4b1f66273ae6c
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  *****************************************************************************/
23 #ifdef HAVE_CONFIG_H
24 # include "config.h"
25 #endif
27 #include <vlc_common.h>
28 #include <vlc_plugin.h>
29 #include <vlc_keystore.h>
31 #include "list_util.h"
33 #include <TargetConditionals.h>
35 #if TARGET_OS_IPHONE
36   #import <Foundation/Foundation.h>
37 #else
38   #import <Cocoa/Cocoa.h>
39 #endif
41 #import <Security/Security.h>
43 // Marker to recognize changed format in vlc 4: secret does not have \0 cut off anymore.
44 int kVlc4Creator = 'vlc4';
46 static int Open(vlc_object_t *);
48 static const int sync_list[] =
49 { 0, 1, 2 };
50 static const char *const sync_list_text[] = {
51     N_("Yes"), N_("No"), N_("Any")
54 static const int accessibility_list[] =
55 { 0, 1, 2, 3, 4, 5, 6, 7 };
56 static const char *const accessibility_list_text[] = {
57     N_("System default"),
58     N_("After first unlock"),
59     N_("After first unlock, on this device only"),
60     N_("Always"),
61     N_("When passcode set, on this device only"),
62     N_("Always, on this device only"),
63     N_("When unlocked"),
64     N_("When unlocked, on this device only")
67 #define SYNC_ITEMS_TEXT N_("Synchronize stored items")
68 #define SYNC_ITEMS_LONGTEXT N_("Synchronizes stored items via iCloud Keychain if enabled in the user domain.")
70 #define ACCESSIBILITY_TYPE_TEXT N_("Accessibility type for all future passwords saved to the Keychain")
72 #define ACCESS_GROUP_TEXT N_("Keychain access group")
73 #define ACCESS_GROUP_LONGTEXT N_("Keychain access group as defined by the app entitlements.")
76 vlc_module_begin()
77     set_shortname(N_("Keychain keystore"))
78     set_description(N_("Keystore for iOS, macOS and tvOS"))
79     set_category(CAT_ADVANCED)
80     set_subcategory(SUBCAT_ADVANCED_MISC)
81     add_integer("keychain-synchronize", 1, SYNC_ITEMS_TEXT, SYNC_ITEMS_LONGTEXT, true)
82     change_integer_list(sync_list, sync_list_text)
83     add_integer("keychain-accessibility-type", 0, ACCESSIBILITY_TYPE_TEXT, ACCESSIBILITY_TYPE_TEXT, true)
84     change_integer_list(accessibility_list, accessibility_list_text)
85     add_string("keychain-access-group", NULL, ACCESS_GROUP_TEXT, ACCESS_GROUP_LONGTEXT, true)
86     set_capability("keystore", 100)
87     set_callback(Open)
88 vlc_module_end ()
90 static NSMutableDictionary * CreateQuery(vlc_keystore *p_keystore)
92     NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:3];
93     [dictionary setObject:(__bridge id)kSecClassInternetPassword forKey:(__bridge id)kSecClass];
95     /* kSecAttrService is only valid for kSecClassGenericPassword but not for
96      * kSecClassInternetPassword. Nevertheless, it is used on macOS for the
97      * wrong password type. It has always worked for now, and changing it would
98      * need to handle a password migration. Using this attribute on iOS cause a
99      * errSecNoSuchAttr error. */
100 #if !TARGET_OS_IPHONE
101     [dictionary setObject:@"VLC-Password-Service" forKey:(__bridge id)kSecAttrService];
102 #endif
104     char * psz_access_group = var_InheritString(p_keystore, "keychain-access-group");
105     if (psz_access_group) {
106         [dictionary setObject:[NSString stringWithUTF8String:psz_access_group] forKey:(__bridge id)kSecAttrAccessGroup];
107     }
108     free(psz_access_group);
110     id syncValue;
111     int syncMode = var_InheritInteger(p_keystore, "keychain-synchronize");
113     if (syncMode == 2) {
114         syncValue = (__bridge id)kSecAttrSynchronizableAny;
115     } else if (syncMode == 0) {
116         syncValue = @(YES);
117     } else {
118         syncValue = @(NO);
119     }
121     [dictionary setObject:syncValue forKey:(__bridge id)(kSecAttrSynchronizable)];
123     return dictionary;
126 static NSString * ErrorForStatus(OSStatus status)
128     NSString *message = nil;
130     switch (status) {
131 #if TARGET_OS_IPHONE
132         case errSecUnimplemented: {
133             message = @"Query unimplemented";
134             break;
135         }
136         case errSecParam: {
137             message = @"Faulty parameter";
138             break;
139         }
140         case errSecAllocate: {
141             message = @"Allocation failure";
142             break;
143         }
144         case errSecNotAvailable: {
145             message = @"Query not available";
146             break;
147         }
148         case errSecDuplicateItem: {
149             message = @"Duplicated item";
150             break;
151         }
152         case errSecItemNotFound: {
153             message = @"Item not found";
154             break;
155         }
156         case errSecInteractionNotAllowed: {
157             message = @"Interaction not allowed";
158             break;
159         }
160         case errSecDecode: {
161             message = @"Decoding failure";
162             break;
163         }
164         case errSecAuthFailed: {
165             message = @"Authentication failure";
166             break;
167         }
168         case -34018: {
169             message = @"iCloud Keychain failure";
170             break;
171         }
172         default: {
173             message = @"Unknown generic error";
174         }
175 #else
176         default:
177             message = (__bridge_transfer NSString *)SecCopyErrorMessageString(status, NULL);
178 #endif
179     }
181     return message;
185 static void SetAccessibilityForQuery(vlc_keystore *p_keystore,
186                                      NSMutableDictionary *query)
188     int accessibilityType = var_InheritInteger(p_keystore, "keychain-accessibility-type");
189     CFStringRef secattr;
190     switch (accessibilityType) {
191         case 1:
192             secattr = kSecAttrAccessibleAfterFirstUnlock;
193             break;
194         case 2:
195             secattr = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
196             break;
197         case 3:
198             secattr = kSecAttrAccessibleAlways;
199             break;
200         case 4:
201             secattr = kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly;
202             break;
203         case 5:
204             secattr = kSecAttrAccessibleAlwaysThisDeviceOnly;
205             break;
206         case 6:
207             secattr = kSecAttrAccessibleWhenUnlocked;
208             break;
209         case 7:
210             secattr = kSecAttrAccessibleWhenUnlockedThisDeviceOnly;
211             break;
212         default:
213             secattr = nil;
214             break;
215     }
216     if (secattr != nil)
217         [query setObject:(__bridge id)secattr forKey:(__bridge id)kSecAttrAccessible];
220 struct vlc2secattr
222     const char *vlc;
223     const CFStringRef secattr;
226 static int vlc2secattr_cmp(const void *key_, const void *entry_)
228     const struct vlc2secattr *entry = entry_;
229     const char *key = key_;
230     return strcasecmp(key, entry->vlc);
233 static int SetAttributesForQuery(const char *const ppsz_values[KEY_MAX], NSMutableDictionary *query, const char *psz_label)
235     const char *psz_protocol = ppsz_values[KEY_PROTOCOL];
236     const char *psz_user = ppsz_values[KEY_USER];
237     const char *psz_server = ppsz_values[KEY_SERVER];
238     const char *psz_path = ppsz_values[KEY_PATH];
239     const char *psz_port = ppsz_values[KEY_PORT];
240     const char *psz_realm = ppsz_values[KEY_REALM];
241     const char *psz_authtype = ppsz_values[KEY_AUTHTYPE];
243     if (psz_label) {
244         [query setObject:[NSString stringWithUTF8String:psz_label] forKey:(__bridge id)kSecAttrLabel];
245     }
246     if (psz_protocol) {
247         const struct vlc2secattr tab[] =
248         { /* /!\ Alphabetical order /!\ */
249             { "ftp", kSecAttrProtocolFTP },
250             { "ftps", kSecAttrProtocolFTPS },
251             { "http", kSecAttrProtocolHTTP },
252             { "https", kSecAttrProtocolHTTPS },
253             { "rtsp", kSecAttrProtocolRTSP },
254             { "sftp", kSecAttrProtocolSSH },
255             { "smb", kSecAttrProtocolSMB },
256         };
258         const struct vlc2secattr *entry =
259             bsearch(psz_protocol, tab, ARRAY_SIZE(tab), sizeof(tab[0]), vlc2secattr_cmp);
260         if (!entry)
261             return VLC_EGENERIC;
263         [query setObject:(__bridge id)entry->secattr forKey:(__bridge id)kSecAttrProtocol];
264     }
265     if (psz_user) {
266         [query setObject:[NSString stringWithUTF8String:psz_user] forKey:(__bridge id)kSecAttrAccount];
267     }
268     if (psz_server) {
269         [query setObject:[NSString stringWithUTF8String:psz_server] forKey:(__bridge id)kSecAttrServer];
270     }
271     if (psz_path) {
272         [query setObject:[NSString stringWithUTF8String:psz_path] forKey:(__bridge id)kSecAttrPath];
273     }
274     if (psz_port) {
275         [query setObject:[NSNumber numberWithInt:atoi(psz_port)] forKey:(__bridge id)kSecAttrPort];
276     }
277     if (psz_realm) {
278         [query setObject:[NSString stringWithUTF8String:psz_realm] forKey:(__bridge id)kSecAttrSecurityDomain];
279     }
280     if (psz_authtype) {
281         if (strncasecmp(psz_protocol, "http", 4) == 0) {
282             const struct vlc2secattr tab[] =
283             { /* /!\ Alphabetical order /!\ */
284                 { "Basic", kSecAttrAuthenticationTypeHTTPBasic },
285                 { "Digest", kSecAttrAuthenticationTypeHTTPDigest },
286             };
287             const struct vlc2secattr *entry =
288                 bsearch(psz_authtype, tab, ARRAY_SIZE(tab), sizeof(tab[0]), vlc2secattr_cmp);
289             if (entry)
290                 [query setObject:(__bridge id)entry->secattr forKey:(__bridge id)kSecAttrAuthenticationType];
291         }
292         else if (strcasecmp(psz_protocol, "smb") == 0) {
293             if (strcmp(psz_authtype, "2") == 0)
294                 [query setObject:(__bridge id)kSecAttrAuthenticationTypeMSN forKey:(__bridge id)kSecAttrAuthenticationType];
295         }
296     }
298     return VLC_SUCCESS;
301 static int FillEntryValues(const NSDictionary *item, char *ppsz_values[KEY_MAX])
303     NSString *protocol = [item objectForKey:(__bridge id)kSecAttrProtocol];
304     if (protocol)
305     {
306         ppsz_values[KEY_PROTOCOL] = strdup([protocol UTF8String]);
307         if (!ppsz_values[KEY_PROTOCOL])
308             return VLC_ENOMEM;
309     }
311     NSString *user = [item objectForKey:(__bridge id)kSecAttrAccount];
312     if (user)
313     {
314         ppsz_values[KEY_USER] = strdup([user UTF8String]);
315         if (!ppsz_values[KEY_USER])
316             return VLC_ENOMEM;
317     }
319     NSString *server = [item objectForKey:(__bridge id)kSecAttrServer];
320     if (server)
321     {
322         ppsz_values[KEY_SERVER] = strdup([server UTF8String]);
323         if (!ppsz_values[KEY_SERVER])
324             return VLC_ENOMEM;
325     }
327     NSString *path = [item objectForKey:(__bridge id)kSecAttrPath];
328     if (path)
329     {
330         ppsz_values[KEY_PATH] = strdup([path UTF8String]);
331         if (!ppsz_values[KEY_PATH])
332             return VLC_ENOMEM;
333     }
335     NSNumber *port = [item objectForKey:(__bridge id)kSecAttrPort];
336     if (port)
337     {
338         ppsz_values[KEY_PORT] = strdup([[port stringValue] UTF8String]);
339         if (!ppsz_values[KEY_PORT])
340             return VLC_ENOMEM;
341     }
343     NSString *realm = [item objectForKey:(__bridge id)kSecAttrSecurityDomain];
344     if (realm)
345     {
346         ppsz_values[KEY_REALM] = strdup([realm UTF8String]);
347         if (!ppsz_values[KEY_REALM])
348             return VLC_ENOMEM;
349     }
351     const char *auth_val = NULL;
352     if ([protocol isEqualToString:(__bridge NSString*)kSecAttrProtocolHTTP]
353      || [protocol isEqualToString:(__bridge NSString*)kSecAttrProtocolHTTPS])
354     {
355         id authtype = [item objectForKey:(__bridge id)kSecAttrAuthenticationType];
356         if (authtype == (__bridge id)kSecAttrAuthenticationTypeHTTPBasic)
357             auth_val = "Basic";
358         else if (authtype == (__bridge id)kSecAttrAuthenticationTypeHTTPDigest)
359             auth_val = "Digest";
360     }
361     else if ([protocol isEqualToString:(__bridge NSString*)kSecAttrProtocolSMB])
362     {
363         id keytype = [item objectForKey:(__bridge id)kSecAttrAuthenticationType];
364         if (keytype == (__bridge id)kSecAttrAuthenticationTypeMSN)
365             auth_val = "2";
366     }
367     if (auth_val)
368     {
369         ppsz_values[KEY_AUTHTYPE] = strdup(auth_val);
370         if (!ppsz_values[KEY_AUTHTYPE])
371             return VLC_ENOMEM;
372     }
374     return VLC_SUCCESS;
377 static int Store(vlc_keystore *p_keystore,
378                  const char *const ppsz_values[KEY_MAX],
379                  const uint8_t *p_secret,
380                  size_t i_secret_len,
381                  const char *psz_label)
383     OSStatus status;
385     if (!ppsz_values[KEY_PROTOCOL] || !p_secret) {
386         return VLC_EGENERIC;
387     }
389     msg_Dbg(p_keystore, "Store keychain entry for server %s", ppsz_values[KEY_SERVER]);
391     NSMutableDictionary *query = nil;
392     NSMutableDictionary *searchQuery = CreateQuery(p_keystore);
394     /* set attributes */
395     if (SetAttributesForQuery(ppsz_values, searchQuery, psz_label))
396         return VLC_EGENERIC;
398     // One return type must be added for SecItemCopyMatching, even if not used.
399     // Older macOS versions (10.7) are very picky here...
400     [searchQuery setObject:@(YES) forKey:(__bridge id)kSecReturnRef];
401     CFTypeRef result = NULL;
403     /* search */
404     status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, &result);
406     if (status == errSecSuccess) {
407         msg_Dbg(p_keystore, "the item was already known to keychain, so it will be updated");
408         /* item already existed in keychain, let's update */
409         query = [[NSMutableDictionary alloc] init];
411         /* just set the secret data */
412         NSData *secretData = [NSData dataWithBytes:p_secret length:i_secret_len];
413         [query setObject:secretData forKey:(__bridge id)kSecValueData];
414         [query setObject:@(kVlc4Creator) forKey:(__bridge id)kSecAttrCreator];
416         status = SecItemUpdate((__bridge CFDictionaryRef)(searchQuery), (__bridge CFDictionaryRef)(query));
417     } else if (status == errSecItemNotFound) {
418         msg_Dbg(p_keystore, "creating new item in keychain");
419         /* item not found, let's create! */
420         query = CreateQuery(p_keystore);
422         /* set attributes */
423         SetAttributesForQuery(ppsz_values, query, psz_label);
425         /* set accessibility */
426         SetAccessibilityForQuery(p_keystore, query);
428         /* set secret data */
429         NSData *secretData = [NSData dataWithBytes:p_secret length:i_secret_len];
430         [query setObject:secretData forKey:(__bridge id)kSecValueData];
431         [query setObject:@(kVlc4Creator) forKey:(__bridge id)kSecAttrCreator];
433         status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
434     }
435     if (status != errSecSuccess) {
436         msg_Err(p_keystore, "Storage failed (%i: '%s')", status, [ErrorForStatus(status) UTF8String]);
437         return VLC_EGENERIC;
438     }
440     return VLC_SUCCESS;
443 static unsigned int Find(vlc_keystore *p_keystore,
444                          const char *const ppsz_values[KEY_MAX],
445                          vlc_keystore_entry **pp_entries)
447     CFTypeRef result = NULL;
448     NSMutableDictionary *baseLookupQuery = CreateQuery(p_keystore);
449     OSStatus status;
451     msg_Dbg(p_keystore, "Lookup keychain entry for server %s", ppsz_values[KEY_SERVER]);
453     /* set attributes */
454     if (SetAttributesForQuery(ppsz_values, baseLookupQuery, NULL))
455         return 0;
457     /* search */
458     NSMutableDictionary *searchQuery = [baseLookupQuery mutableCopy];
459     [searchQuery setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];
460     [searchQuery setObject:(__bridge id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit];
462     status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, &result);
463     if (status != errSecSuccess) {
464         msg_Warn(p_keystore, "lookup failed (%i: '%s')", status, [ErrorForStatus(status) UTF8String]);
465         return 0;
466     }
468     NSArray *listOfResults = (__bridge_transfer NSArray *)result;
469     NSUInteger count = listOfResults.count;
470     msg_Dbg(p_keystore, "found %lu result(s) for the provided attributes", count);
472     vlc_keystore_entry *p_entries = calloc(count,
473                                            sizeof(vlc_keystore_entry));
474     if (!p_entries)
475         return 0;
477     for (NSUInteger i = 0; i < count; i++) {
478         vlc_keystore_entry *p_entry = &p_entries[i];
479         NSDictionary *keychainItem = [listOfResults objectAtIndex:i];
481         if (FillEntryValues(keychainItem, p_entry->ppsz_values))
482         {
483             vlc_keystore_release_entries(p_entries, 1);
484             return 0;
485         }
487         NSString *accountName = [keychainItem objectForKey:(__bridge id)kSecAttrAccount];
488         NSMutableDictionary *passwordFetchQuery = [baseLookupQuery mutableCopy];
489         [passwordFetchQuery setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
490         [passwordFetchQuery setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
491         if (accountName)
492             [passwordFetchQuery setObject:accountName forKey:(__bridge id)kSecAttrAccount];
494         CFTypeRef secretResult = NULL;
495         status = SecItemCopyMatching((__bridge CFDictionaryRef)passwordFetchQuery, &secretResult);
496         if (status != noErr) {
497             msg_Err(p_keystore, "Lookup error: %i (%s)", status, [ErrorForStatus(status) UTF8String]);
498             vlc_keystore_release_entries(p_entries, (unsigned int)count);
499             return 0;
500         }
502         NSData *secretData = (__bridge_transfer NSData *)secretResult;
503         NSNumber *creator = [keychainItem objectForKey:(__bridge id)kSecAttrCreator];
504         if (creator && [creator isEqual:@(kVlc4Creator)]) {
505             msg_Dbg(p_keystore, "Found keychain entry in vlc4 format");
506             vlc_keystore_entry_set_secret(p_entry, secretData.bytes, secretData.length);
508         } else {
509             msg_Dbg(p_keystore, "Found keychain entry in vlc3 format");
511             /* we need to do some padding here, as string is expected to be 0 terminated */
512             NSUInteger secretDataLength = secretData.length;
513             uint8_t *paddedSecretData = calloc(1, secretDataLength + 1);
514             memcpy(paddedSecretData, secretData.bytes, secretDataLength);
515             vlc_keystore_entry_set_secret(p_entry, paddedSecretData, secretDataLength + 1);
516             free(paddedSecretData);
517         }
519         vlc_keystore_entry_set_secret(p_entry, secretData.bytes, secretData.length);
520     }
522     *pp_entries = p_entries;
524     return (unsigned int)count;
527 static unsigned int Remove(vlc_keystore *p_keystore,
528                            const char *const ppsz_values[KEY_MAX])
530     CFTypeRef result = NULL;
531     NSMutableDictionary *query = CreateQuery(p_keystore);
532     OSStatus status;
534     if (SetAttributesForQuery(ppsz_values, query, NULL))
535         return 0;
537     [query setObject:@(YES) forKey:(__bridge id)kSecReturnAttributes];
538     [query setObject:(__bridge id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit];
540     /* do a copy matching to see how many items we are going to delete */
541     status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
542     if (status != errSecSuccess) {
543         msg_Warn(p_keystore, "lookup failed (%i: '%s')", status, [ErrorForStatus(status) UTF8String]);
544         return 0;
545     }
547     NSArray *listOfResults = (__bridge_transfer NSArray *)result;
548     NSUInteger count = listOfResults.count;
549     msg_Dbg(p_keystore, "found %lu result(s) for the provided attributes", count);
551     /* delete everything!! */
552     status = SecItemDelete((__bridge CFDictionaryRef)query);
554     if (status != errSecSuccess) {
555         msg_Err(p_keystore, "deleting items matching the provided attributes failed");
556         return VLC_EGENERIC;
557     }
559     return (unsigned int)count;
562 static int Open(vlc_object_t *p_this)
564     vlc_keystore *p_keystore = (vlc_keystore *)p_this;
566     p_keystore->p_sys = NULL;
567     p_keystore->pf_store = Store;
568     p_keystore->pf_find = Find;
569     p_keystore->pf_remove = Remove;
571     return VLC_SUCCESS;