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