1 /*****************************************************************************
2 * keychain.m: Darwin Keychain keystore module
3 *****************************************************************************
4 * Copyright © 2016, 2018 VLC authors, VideoLAN and VideoLabs
6 * Author: Felix Paul Kühne <fkuehne # videolabs.io>
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.
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.
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 *****************************************************************************/
27 #include <vlc_common.h>
28 #include <vlc_plugin.h>
29 #include <vlc_keystore.h>
31 #include "list_util.h"
33 #include <TargetConditionals.h>
36 #import <Foundation/Foundation.h>
38 #import <Cocoa/Cocoa.h>
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[] =
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[] = {
58 N_("After first unlock"),
59 N_("After first unlock, on this device only"),
61 N_("When passcode set, on this device only"),
62 N_("Always, on this device only"),
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.")
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)
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];
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];
108 free(psz_access_group);
111 int syncMode = var_InheritInteger(p_keystore, "keychain-synchronize");
114 syncValue = (__bridge id)kSecAttrSynchronizableAny;
115 } else if (syncMode == 0) {
121 [dictionary setObject:syncValue forKey:(__bridge id)(kSecAttrSynchronizable)];
126 static NSString * ErrorForStatus(OSStatus status)
128 NSString *message = nil;
132 case errSecUnimplemented: {
133 message = @"Query unimplemented";
137 message = @"Faulty parameter";
140 case errSecAllocate: {
141 message = @"Allocation failure";
144 case errSecNotAvailable: {
145 message = @"Query not available";
148 case errSecDuplicateItem: {
149 message = @"Duplicated item";
152 case errSecItemNotFound: {
153 message = @"Item not found";
156 case errSecInteractionNotAllowed: {
157 message = @"Interaction not allowed";
161 message = @"Decoding failure";
164 case errSecAuthFailed: {
165 message = @"Authentication failure";
169 message = @"iCloud Keychain failure";
173 message = @"Unknown generic error";
177 message = (__bridge_transfer NSString *)SecCopyErrorMessageString(status, NULL);
185 static void SetAccessibilityForQuery(vlc_keystore *p_keystore,
186 NSMutableDictionary *query)
188 int accessibilityType = var_InheritInteger(p_keystore, "keychain-accessibility-type");
190 switch (accessibilityType) {
192 secattr = kSecAttrAccessibleAfterFirstUnlock;
195 secattr = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
198 secattr = kSecAttrAccessibleAlways;
201 secattr = kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly;
204 secattr = kSecAttrAccessibleAlwaysThisDeviceOnly;
207 secattr = kSecAttrAccessibleWhenUnlocked;
210 secattr = kSecAttrAccessibleWhenUnlockedThisDeviceOnly;
217 [query setObject:(__bridge id)secattr forKey:(__bridge id)kSecAttrAccessible];
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];
244 [query setObject:[NSString stringWithUTF8String:psz_label] forKey:(__bridge id)kSecAttrLabel];
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 },
258 const struct vlc2secattr *entry =
259 bsearch(psz_protocol, tab, ARRAY_SIZE(tab), sizeof(tab[0]), vlc2secattr_cmp);
263 [query setObject:(__bridge id)entry->secattr forKey:(__bridge id)kSecAttrProtocol];
266 [query setObject:[NSString stringWithUTF8String:psz_user] forKey:(__bridge id)kSecAttrAccount];
269 [query setObject:[NSString stringWithUTF8String:psz_server] forKey:(__bridge id)kSecAttrServer];
272 [query setObject:[NSString stringWithUTF8String:psz_path] forKey:(__bridge id)kSecAttrPath];
275 [query setObject:[NSNumber numberWithInt:atoi(psz_port)] forKey:(__bridge id)kSecAttrPort];
278 [query setObject:[NSString stringWithUTF8String:psz_realm] forKey:(__bridge id)kSecAttrSecurityDomain];
281 if (strncasecmp(psz_protocol, "http", 4) == 0) {
282 const struct vlc2secattr tab[] =
283 { /* /!\ Alphabetical order /!\ */
284 { "Basic", kSecAttrAuthenticationTypeHTTPBasic },
285 { "Digest", kSecAttrAuthenticationTypeHTTPDigest },
287 const struct vlc2secattr *entry =
288 bsearch(psz_authtype, tab, ARRAY_SIZE(tab), sizeof(tab[0]), vlc2secattr_cmp);
290 [query setObject:(__bridge id)entry->secattr forKey:(__bridge id)kSecAttrAuthenticationType];
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];
301 static int FillEntryValues(const NSDictionary *item, char *ppsz_values[KEY_MAX])
303 NSString *protocol = [item objectForKey:(__bridge id)kSecAttrProtocol];
306 ppsz_values[KEY_PROTOCOL] = strdup([protocol UTF8String]);
307 if (!ppsz_values[KEY_PROTOCOL])
311 NSString *user = [item objectForKey:(__bridge id)kSecAttrAccount];
314 ppsz_values[KEY_USER] = strdup([user UTF8String]);
315 if (!ppsz_values[KEY_USER])
319 NSString *server = [item objectForKey:(__bridge id)kSecAttrServer];
322 ppsz_values[KEY_SERVER] = strdup([server UTF8String]);
323 if (!ppsz_values[KEY_SERVER])
327 NSString *path = [item objectForKey:(__bridge id)kSecAttrPath];
330 ppsz_values[KEY_PATH] = strdup([path UTF8String]);
331 if (!ppsz_values[KEY_PATH])
335 NSNumber *port = [item objectForKey:(__bridge id)kSecAttrPort];
338 ppsz_values[KEY_PORT] = strdup([[port stringValue] UTF8String]);
339 if (!ppsz_values[KEY_PORT])
343 NSString *realm = [item objectForKey:(__bridge id)kSecAttrSecurityDomain];
346 ppsz_values[KEY_REALM] = strdup([realm UTF8String]);
347 if (!ppsz_values[KEY_REALM])
351 const char *auth_val = NULL;
352 if ([protocol isEqualToString:(__bridge NSString*)kSecAttrProtocolHTTP]
353 || [protocol isEqualToString:(__bridge NSString*)kSecAttrProtocolHTTPS])
355 id authtype = [item objectForKey:(__bridge id)kSecAttrAuthenticationType];
356 if (authtype == (__bridge id)kSecAttrAuthenticationTypeHTTPBasic)
358 else if (authtype == (__bridge id)kSecAttrAuthenticationTypeHTTPDigest)
361 else if ([protocol isEqualToString:(__bridge NSString*)kSecAttrProtocolSMB])
363 id keytype = [item objectForKey:(__bridge id)kSecAttrAuthenticationType];
364 if (keytype == (__bridge id)kSecAttrAuthenticationTypeMSN)
369 ppsz_values[KEY_AUTHTYPE] = strdup(auth_val);
370 if (!ppsz_values[KEY_AUTHTYPE])
377 static int Store(vlc_keystore *p_keystore,
378 const char *const ppsz_values[KEY_MAX],
379 const uint8_t *p_secret,
381 const char *psz_label)
385 if (!ppsz_values[KEY_PROTOCOL] || !p_secret) {
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);
395 if (SetAttributesForQuery(ppsz_values, searchQuery, psz_label))
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;
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);
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);
435 if (status != errSecSuccess) {
436 msg_Err(p_keystore, "Storage failed (%i: '%s')", status, [ErrorForStatus(status) UTF8String]);
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);
451 msg_Dbg(p_keystore, "Lookup keychain entry for server %s", ppsz_values[KEY_SERVER]);
454 if (SetAttributesForQuery(ppsz_values, baseLookupQuery, NULL))
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]);
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));
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))
483 vlc_keystore_release_entries(p_entries, 1);
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];
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);
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);
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);
519 vlc_keystore_entry_set_secret(p_entry, secretData.bytes, secretData.length);
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);
534 if (SetAttributesForQuery(ppsz_values, query, NULL))
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]);
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");
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;