| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "device/fido/mac/credential_store.h" |
| |
| #include <CoreFoundation/CoreFoundation.h> |
| #import <Foundation/Foundation.h> |
| #import <LocalAuthentication/LocalAuthentication.h> |
| #import <Security/Security.h> |
| |
| #include "base/apple/bridging.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/cxx20_erase.h" |
| #include "base/feature_list.h" |
| #include "base/logging.h" |
| #include "base/mac/foundation_util.h" |
| #include "base/mac/mac_logging.h" |
| #include "base/mac/scoped_cftyperef.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "components/device_event_log/device_event_log.h" |
| #include "crypto/random.h" |
| #include "device/fido/authenticator_data.h" |
| #include "device/fido/fido_parsing_utils.h" |
| #include "device/fido/mac/credential_metadata.h" |
| #include "device/fido/mac/keychain.h" |
| #include "device/fido/mac/touch_id_context.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace device::fido::mac { |
| |
| namespace { |
| |
| // DefaultKeychainQuery returns a default keychain query dictionary that has |
| // the keychain item class, keychain access group and RP ID (unless `rp_id` is |
| // `nullopt`) filled out. More fields can be set on the return value to refine |
| // the query. |
| base::ScopedCFTypeRef<CFMutableDictionaryRef> DefaultKeychainQuery( |
| const AuthenticatorConfig& config, |
| absl::optional<std::string> rp_id) { |
| base::ScopedCFTypeRef<CFMutableDictionaryRef> query(CFDictionaryCreateMutable( |
| kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, |
| &kCFTypeDictionaryValueCallBacks)); |
| CFDictionarySetValue(query, kSecClass, kSecClassKey); |
| CFDictionarySetValue( |
| query, kSecAttrAccessGroup, |
| base::SysUTF8ToCFStringRef(config.keychain_access_group)); |
| if (rp_id) { |
| CFDictionarySetValue( |
| query, kSecAttrLabel, |
| base::SysUTF8ToCFStringRef(EncodeRpId(config.metadata_secret, *rp_id))); |
| } |
| return query; |
| } |
| |
| // Erase all keychain items with a creation date that is not within [not_before, |
| // not_after). |
| void FilterKeychainItemsByCreationDate( |
| std::vector<base::ScopedCFTypeRef<CFDictionaryRef>>* keychain_items, |
| base::Time not_before, |
| base::Time not_after) { |
| base::EraseIf( |
| *keychain_items, |
| [not_before, not_after](const CFDictionaryRef& attributes) -> bool { |
| // If the creation date is missing for some obscure reason, treat as if |
| // the date is inside the interval, i.e. keep it in the list. |
| CFDateRef creation_date_cf = |
| base::mac::GetValueFromDictionary<CFDateRef>(attributes, |
| kSecAttrCreationDate); |
| if (!creation_date_cf) { |
| return false; |
| } |
| base::Time creation_date = base::Time::FromCFAbsoluteTime( |
| CFDateGetAbsoluteTime(creation_date_cf)); |
| return creation_date < not_before || creation_date >= not_after; |
| }); |
| } |
| |
| absl::optional<std::vector<base::ScopedCFTypeRef<CFDictionaryRef>>> |
| QueryKeychainItemsForProfile(const std::string& keychain_access_group, |
| const std::string& metadata_secret, |
| base::Time created_not_before, |
| base::Time created_not_after) { |
| // Query the keychain for all items tagged with the given access group, which |
| // should in theory yield all WebAuthentication credentials (for all |
| // profiles). Sadly, the kSecAttrAccessGroup filter doesn't quite work, and |
| // so we also get results from the legacy keychain that are tagged with no |
| // keychain access group. |
| std::vector<base::ScopedCFTypeRef<CFDictionaryRef>> result; |
| |
| base::ScopedCFTypeRef<CFMutableDictionaryRef> query(CFDictionaryCreateMutable( |
| kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, |
| &kCFTypeDictionaryValueCallBacks)); |
| CFDictionarySetValue(query, kSecClass, kSecClassKey); |
| CFDictionarySetValue(query, kSecAttrAccessGroup, |
| base::SysUTF8ToCFStringRef(keychain_access_group)); |
| CFDictionarySetValue(query, kSecMatchLimit, kSecMatchLimitAll); |
| // Return the key reference and its attributes. |
| CFDictionarySetValue(query, kSecReturnRef, kCFBooleanTrue); |
| CFDictionarySetValue(query, kSecReturnAttributes, kCFBooleanTrue); |
| |
| base::ScopedCFTypeRef<CFArrayRef> keychain_items; |
| { |
| OSStatus status = Keychain::GetInstance().ItemCopyMatching( |
| query, reinterpret_cast<CFTypeRef*>(keychain_items.InitializeInto())); |
| if (status == errSecItemNotFound) { |
| DVLOG(1) << "no credentials found"; |
| return absl::nullopt; |
| } |
| if (status != errSecSuccess) { |
| OSSTATUS_DLOG(ERROR, status) << "SecItemCopyMatching failed"; |
| return absl::nullopt; |
| } |
| } |
| |
| for (CFIndex i = 0; i < CFArrayGetCount(keychain_items); ++i) { |
| CFDictionaryRef attributes = base::mac::CFCast<CFDictionaryRef>( |
| CFArrayGetValueAtIndex(keychain_items, i)); |
| if (!attributes) { |
| DLOG(ERROR) << "unexpected result type"; |
| return absl::nullopt; |
| } |
| |
| // Skip items that don't belong to the correct keychain access group |
| // because the kSecAttrAccessGroup filter is broken. |
| CFStringRef attr_access_group = |
| base::mac::GetValueFromDictionary<CFStringRef>(attributes, |
| kSecAttrAccessGroup); |
| if (!attr_access_group || base::SysCFStringRefToUTF8(attr_access_group) != |
| keychain_access_group) { |
| DVLOG(1) << "missing/invalid access group"; |
| continue; |
| } |
| |
| // If the RP ID, stored encrypted in the item's label, cannot be decrypted |
| // with the given metadata secret, then the credential belongs to a |
| // different profile and must be ignored. |
| CFStringRef sec_attr_label = base::mac::GetValueFromDictionary<CFStringRef>( |
| attributes, kSecAttrLabel); |
| if (!sec_attr_label) { |
| DLOG(ERROR) << "missing label"; |
| continue; |
| } |
| absl::optional<std::string> opt_rp_id = |
| DecodeRpId(metadata_secret, base::SysCFStringRefToUTF8(sec_attr_label)); |
| if (!opt_rp_id) { |
| DVLOG(1) << "key doesn't belong to this profile"; |
| continue; |
| } |
| |
| result.emplace_back(attributes, base::scoped_policy::RETAIN); |
| } |
| |
| FilterKeychainItemsByCreationDate(&result, created_not_before, |
| created_not_after); |
| return result; |
| } |
| |
| std::vector<uint8_t> GenerateRandomCredentialId() { |
| // The length of CredentialMetadata::Version::kV3 credentials. Older |
| // credentials use the sealed metadata as the ID, which varies in size. |
| constexpr size_t kCredentialIdLength = 32; |
| std::vector<uint8_t> id(kCredentialIdLength); |
| crypto::RandBytes(id); |
| return id; |
| } |
| |
| } // namespace |
| |
| Credential::Credential(base::ScopedCFTypeRef<SecKeyRef> private_key, |
| std::vector<uint8_t> credential_id, |
| CredentialMetadata metadata, |
| std::string rp_id) |
| : private_key(std::move(private_key)), |
| credential_id(std::move(credential_id)), |
| metadata(std::move(metadata)), |
| rp_id(rp_id) {} |
| |
| Credential::Credential(const Credential& other) = default; |
| |
| Credential::Credential(Credential&& other) = default; |
| |
| Credential& Credential::operator=(const Credential& other) = default; |
| |
| Credential& Credential::operator=(Credential&& other) = default; |
| |
| Credential::~Credential() = default; |
| |
| bool Credential::operator==(const Credential& other) const { |
| return CFEqual(private_key, other.private_key) && |
| credential_id == other.credential_id && metadata == other.metadata; |
| } |
| |
| bool Credential::RequiresUvForSignature() const { |
| return metadata.version < CredentialMetadata::Version::kV4; |
| } |
| |
| struct TouchIdCredentialStore::ObjCStorage { |
| LAContext* __strong authentication_context; |
| }; |
| |
| TouchIdCredentialStore::TouchIdCredentialStore(AuthenticatorConfig config) |
| : config_(std::move(config)), |
| objc_storage_(std::make_unique<ObjCStorage>()) {} |
| TouchIdCredentialStore::~TouchIdCredentialStore() = default; |
| |
| void TouchIdCredentialStore::SetAuthenticationContext( |
| LAContext* authentication_context) { |
| objc_storage_->authentication_context = authentication_context; |
| } |
| |
| absl::optional<std::pair<Credential, base::ScopedCFTypeRef<SecKeyRef>>> |
| TouchIdCredentialStore::CreateCredential( |
| const std::string& rp_id, |
| const PublicKeyCredentialUserEntity& user, |
| Discoverable discoverable) const { |
| base::ScopedCFTypeRef<CFMutableDictionaryRef> params( |
| CFDictionaryCreateMutable(kCFAllocatorDefault, 0, |
| &kCFTypeDictionaryKeyCallBacks, |
| &kCFTypeDictionaryValueCallBacks)); |
| CFDictionarySetValue( |
| params, kSecAttrAccessGroup, |
| base::SysUTF8ToCFStringRef(config_.keychain_access_group)); |
| CFDictionarySetValue(params, kSecAttrKeyType, |
| kSecAttrKeyTypeECSECPrimeRandom); |
| CFDictionarySetValue(params, kSecAttrKeySizeInBits, |
| base::apple::NSToCFPtrCast(@256)); |
| CFDictionarySetValue(params, kSecAttrSynchronizable, kCFBooleanFalse); |
| CFDictionarySetValue(params, kSecAttrTokenID, kSecAttrTokenIDSecureEnclave); |
| |
| CFDictionarySetValue( |
| params, kSecAttrLabel, |
| base::SysUTF8ToCFStringRef(EncodeRpId(config_.metadata_secret, rp_id))); |
| auto credential_metadata = |
| CredentialMetadata::FromPublicKeyCredentialUserEntity( |
| user, discoverable == kDiscoverable); |
| const std::vector<uint8_t> sealed_metadata = SealCredentialMetadata( |
| config_.metadata_secret, rp_id, credential_metadata); |
| CFDictionarySetValue(params, kSecAttrApplicationTag, |
| base::apple::NSToCFPtrCast([NSData |
| dataWithBytes:sealed_metadata.data() |
| length:sealed_metadata.size()])); |
| const std::vector<uint8_t> credential_id = GenerateRandomCredentialId(); |
| CFDictionarySetValue( |
| params, kSecAttrApplicationLabel, |
| base::apple::NSToCFPtrCast([NSData dataWithBytes:credential_id.data() |
| length:credential_id.size()])); |
| base::ScopedCFTypeRef<CFMutableDictionaryRef> private_key_params( |
| CFDictionaryCreateMutable(kCFAllocatorDefault, 0, |
| &kCFTypeDictionaryKeyCallBacks, |
| &kCFTypeDictionaryValueCallBacks)); |
| CFDictionarySetValue(params, kSecPrivateKeyAttrs, private_key_params); |
| CFDictionarySetValue(private_key_params, kSecAttrIsPermanent, kCFBooleanTrue); |
| // The credential can only be used for signing, and the device needs to be in |
| // an unlocked state. |
| auto flags = |
| base::FeatureList::IsEnabled(kWebAuthnMacPlatformAuthenticatorOptionalUv) |
| ? kSecAccessControlPrivateKeyUsage |
| : kSecAccessControlPrivateKeyUsage | kSecAccessControlUserPresence; |
| base::ScopedCFTypeRef<SecAccessControlRef> access_control( |
| SecAccessControlCreateWithFlags( |
| kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, |
| flags, /*error=*/nullptr)); |
| CFDictionarySetValue(private_key_params, kSecAttrAccessControl, |
| access_control); |
| if (objc_storage_->authentication_context) { |
| CFDictionarySetValue( |
| private_key_params, kSecUseAuthenticationContext, |
| (__bridge CFTypeRef)objc_storage_->authentication_context); |
| } |
| base::ScopedCFTypeRef<CFErrorRef> cferr; |
| base::ScopedCFTypeRef<SecKeyRef> private_key = |
| Keychain::GetInstance().KeyCreateRandomKey(params, |
| cferr.InitializeInto()); |
| if (!private_key) { |
| FIDO_LOG(ERROR) << "SecKeyCreateRandomKey failed: " << cferr; |
| return absl::nullopt; |
| } |
| base::ScopedCFTypeRef<SecKeyRef> public_key( |
| Keychain::GetInstance().KeyCopyPublicKey(private_key)); |
| if (!public_key) { |
| FIDO_LOG(ERROR) << "SecKeyCopyPublicKey failed"; |
| return absl::nullopt; |
| } |
| |
| return std::make_pair( |
| Credential(std::move(private_key), std::move(credential_id), |
| std::move(credential_metadata), std::move(rp_id)), |
| std::move(public_key)); |
| } |
| |
| absl::optional<std::pair<Credential, base::ScopedCFTypeRef<SecKeyRef>>> |
| TouchIdCredentialStore::CreateCredentialLegacyCredentialForTesting( |
| CredentialMetadata::Version version, |
| const std::string& rp_id, |
| const PublicKeyCredentialUserEntity& user, |
| Discoverable discoverable) const { |
| DCHECK(discoverable == Discoverable::kNonDiscoverable || |
| version > CredentialMetadata::Version::kV0); |
| |
| const bool is_discoverable = discoverable == Discoverable::kDiscoverable; |
| std::vector<uint8_t> credential_id = SealLegacyCredentialIdForTestingOnly( |
| version, config_.metadata_secret, rp_id, user.id, user.name.value_or(""), |
| user.display_name.value_or(""), is_discoverable); |
| absl::optional<CredentialMetadata> metadata = |
| UnsealMetadataFromLegacyCredentialId(config_.metadata_secret, rp_id, |
| credential_id); |
| DCHECK(metadata); |
| |
| base::ScopedCFTypeRef<CFMutableDictionaryRef> params( |
| CFDictionaryCreateMutable(kCFAllocatorDefault, 0, |
| &kCFTypeDictionaryKeyCallBacks, |
| &kCFTypeDictionaryValueCallBacks)); |
| CFDictionarySetValue( |
| params, kSecAttrAccessGroup, |
| base::SysUTF8ToCFStringRef(config_.keychain_access_group)); |
| CFDictionarySetValue(params, kSecAttrKeyType, |
| kSecAttrKeyTypeECSECPrimeRandom); |
| CFDictionarySetValue(params, kSecAttrKeySizeInBits, |
| base::apple::NSToCFPtrCast(@256)); |
| CFDictionarySetValue(params, kSecAttrSynchronizable, kCFBooleanFalse); |
| CFDictionarySetValue(params, kSecAttrTokenID, kSecAttrTokenIDSecureEnclave); |
| |
| CFDictionarySetValue( |
| params, kSecAttrLabel, |
| base::SysUTF8ToCFStringRef(EncodeRpId(config_.metadata_secret, rp_id))); |
| CFDictionarySetValue(params, kSecAttrApplicationTag, |
| base::SysUTF8ToCFStringRef(EncodeRpIdAndUserIdDeprecated( |
| config_.metadata_secret, rp_id, user.id))); |
| CFDictionarySetValue( |
| params, kSecAttrApplicationLabel, |
| base::apple::NSToCFPtrCast([NSData dataWithBytes:credential_id.data() |
| length:credential_id.size()])); |
| base::ScopedCFTypeRef<CFMutableDictionaryRef> private_key_params( |
| CFDictionaryCreateMutable(kCFAllocatorDefault, 0, |
| &kCFTypeDictionaryKeyCallBacks, |
| &kCFTypeDictionaryValueCallBacks)); |
| CFDictionarySetValue(params, kSecPrivateKeyAttrs, private_key_params); |
| CFDictionarySetValue(private_key_params, kSecAttrIsPermanent, kCFBooleanTrue); |
| // Credential can only be used when the device is unlocked. Private key is |
| // available for signing after user authorization with biometrics or |
| // password. |
| base::ScopedCFTypeRef<SecAccessControlRef> access_control( |
| SecAccessControlCreateWithFlags( |
| kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, |
| kSecAccessControlPrivateKeyUsage | kSecAccessControlUserPresence, |
| /*error=*/nullptr)); |
| CFDictionarySetValue(private_key_params, kSecAttrAccessControl, |
| access_control); |
| if (objc_storage_->authentication_context) { |
| CFDictionarySetValue( |
| private_key_params, kSecUseAuthenticationContext, |
| (__bridge CFTypeRef)objc_storage_->authentication_context); |
| } |
| base::ScopedCFTypeRef<CFErrorRef> cferr; |
| base::ScopedCFTypeRef<SecKeyRef> private_key = |
| Keychain::GetInstance().KeyCreateRandomKey(params, |
| cferr.InitializeInto()); |
| if (!private_key) { |
| FIDO_LOG(ERROR) << "SecKeyCreateRandomKey failed: " << cferr; |
| return absl::nullopt; |
| } |
| base::ScopedCFTypeRef<SecKeyRef> public_key( |
| Keychain::GetInstance().KeyCopyPublicKey(private_key)); |
| if (!public_key) { |
| FIDO_LOG(ERROR) << "SecKeyCopyPublicKey failed"; |
| return absl::nullopt; |
| } |
| |
| return std::make_pair( |
| Credential(std::move(private_key), std::move(credential_id), |
| std::move(*metadata), std::move(rp_id)), |
| std::move(public_key)); |
| } |
| |
| absl::optional<std::list<Credential>> |
| TouchIdCredentialStore::FindCredentialsFromCredentialDescriptorList( |
| const std::string& rp_id, |
| const std::vector<PublicKeyCredentialDescriptor>& descriptors) const { |
| std::set<std::vector<uint8_t>> credential_ids; |
| for (const auto& descriptor : descriptors) { |
| if (descriptor.credential_type == CredentialType::kPublicKey && |
| (descriptor.transports.empty() || |
| base::Contains(descriptor.transports, |
| FidoTransportProtocol::kInternal))) { |
| credential_ids.insert(descriptor.id); |
| } |
| } |
| if (credential_ids.empty()) { |
| // Don't call FindCredentialsImpl(). Given an empty |credential_ids|, it |
| // returns *all* credentials for |rp_id|. |
| return std::list<Credential>(); |
| } |
| return FindCredentialsImpl(rp_id, credential_ids); |
| } |
| |
| absl::optional<std::list<Credential>> |
| TouchIdCredentialStore::FindResidentCredentials( |
| const absl::optional<std::string>& rp_id) const { |
| absl::optional<std::list<Credential>> credentials = |
| FindCredentialsImpl(rp_id, /*credential_ids=*/{}); |
| if (!credentials) { |
| return absl::nullopt; |
| } |
| credentials->remove_if([](const Credential& credential) { |
| return !credential.metadata.is_resident; |
| }); |
| return credentials; |
| } |
| |
| bool TouchIdCredentialStore::DeleteCredentialsForUserId( |
| const std::string& rp_id, |
| const std::vector<uint8_t>& user_id) const { |
| absl::optional<std::list<Credential>> credentials = |
| FindCredentialsImpl(rp_id, /*credential_ids=*/{}); |
| if (!credentials) { |
| return false; |
| } |
| for (const Credential& credential : *credentials) { |
| if (user_id != credential.metadata.user_id) { |
| continue; |
| } |
| if (!DeleteCredentialById(credential.credential_id)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| void TouchIdCredentialStore::DeleteCredentials(base::Time created_not_before, |
| base::Time created_not_after, |
| base::OnceClosure callback) { |
| DeleteCredentialsSync(created_not_before, created_not_after); |
| std::move(callback).Run(); |
| } |
| |
| void TouchIdCredentialStore::CountCredentials( |
| base::Time created_not_before, |
| base::Time created_not_after, |
| base::OnceCallback<void(size_t)> callback) { |
| std::move(callback).Run( |
| CountCredentialsSync(created_not_before, created_not_after)); |
| } |
| |
| bool TouchIdCredentialStore::DeleteCredentialsSync( |
| base::Time created_not_before, |
| base::Time created_not_after) { |
| absl::optional<std::vector<base::ScopedCFTypeRef<CFDictionaryRef>>> |
| keychain_items = QueryKeychainItemsForProfile( |
| config_.keychain_access_group, config_.metadata_secret, |
| created_not_before, created_not_after); |
| if (!keychain_items) { |
| return false; |
| } |
| |
| bool result = true; |
| for (const base::ScopedCFTypeRef<CFDictionaryRef>& attributes : |
| *keychain_items) { |
| // kSecAttrApplicationLabel stores the credential ID. |
| CFDataRef credential_id_data = base::mac::GetValueFromDictionary<CFDataRef>( |
| attributes.get(), kSecAttrApplicationLabel); |
| if (!credential_id_data) { |
| DLOG(ERROR) << "missing application label"; |
| continue; |
| } |
| if (!DeleteCredentialById(base::make_span( |
| CFDataGetBytePtr(credential_id_data), |
| base::checked_cast<size_t>(CFDataGetLength(credential_id_data))))) { |
| // Indicate failure, but keep deleting remaining items. |
| result = false; |
| } |
| } |
| return result; |
| } |
| |
| size_t TouchIdCredentialStore::CountCredentialsSync( |
| base::Time created_not_before, |
| base::Time created_not_after) { |
| absl::optional<std::vector<base::ScopedCFTypeRef<CFDictionaryRef>>> |
| keychain_items = QueryKeychainItemsForProfile( |
| config_.keychain_access_group, config_.metadata_secret, |
| created_not_before, created_not_after); |
| if (!keychain_items) { |
| DLOG(ERROR) << "Failed to query credentials in keychain"; |
| return 0; |
| } |
| return keychain_items->size(); |
| } |
| |
| // static |
| std::vector<Credential> TouchIdCredentialStore::FindCredentialsForTesting( |
| AuthenticatorConfig config, |
| std::string rp_id) { |
| TouchIdCredentialStore store(std::move(config)); |
| absl::optional<std::list<Credential>> credentials = |
| store.FindCredentialsImpl(rp_id, /*credential_ids=*/{}); |
| DCHECK(credentials) << "FindCredentialsImpl shouldn't fail in tests"; |
| std::vector<Credential> result; |
| for (Credential& credential : *credentials) { |
| result.emplace_back(std::move(credential)); |
| } |
| return result; |
| } |
| |
| absl::optional<std::list<Credential>> |
| TouchIdCredentialStore::FindCredentialsImpl( |
| const absl::optional<std::string>& rp_id, |
| const std::set<std::vector<uint8_t>>& credential_ids) const { |
| // Query all credentials for the RP. Filtering for `rp_id` here ensures we |
| // don't retrieve credentials for other profiles, because their |
| // `kSecAttrLabel` attribute wouldn't match the encoded RP ID. |
| base::ScopedCFTypeRef<CFMutableDictionaryRef> query = |
| DefaultKeychainQuery(config_, rp_id); |
| if (objc_storage_->authentication_context) { |
| CFDictionarySetValue( |
| query, kSecUseAuthenticationContext, |
| (__bridge CFTypeRef)objc_storage_->authentication_context); |
| } |
| CFDictionarySetValue(query, kSecReturnRef, kCFBooleanTrue); |
| CFDictionarySetValue(query, kSecReturnAttributes, kCFBooleanTrue); |
| CFDictionarySetValue(query, kSecMatchLimit, kSecMatchLimitAll); |
| |
| base::ScopedCFTypeRef<CFArrayRef> keychain_items; |
| OSStatus status = Keychain::GetInstance().ItemCopyMatching( |
| query, reinterpret_cast<CFTypeRef*>(keychain_items.InitializeInto())); |
| if (status == errSecItemNotFound) { |
| return std::list<Credential>(); |
| } |
| if (status != errSecSuccess) { |
| FIDO_LOG(ERROR) << "SecItemCopyMatching failed: " |
| << logging::DescriptionFromOSStatus(status); |
| return absl::nullopt; |
| } |
| |
| // Filter credentials for the RP down to |credential_ids|, unless it's |
| // empty in which case all credentials should be returned. |
| std::list<Credential> credentials; |
| for (CFIndex i = 0; i < CFArrayGetCount(keychain_items); ++i) { |
| CFDictionaryRef attributes = base::mac::CFCast<CFDictionaryRef>( |
| CFArrayGetValueAtIndex(keychain_items, i)); |
| if (!attributes) { |
| FIDO_LOG(ERROR) << "credential with missing attributes"; |
| return absl::nullopt; |
| } |
| // Skip items that don't belong to the correct keychain access group |
| // because the kSecAttrAccessGroup filter is broken. |
| CFStringRef attr_access_group = |
| base::mac::GetValueFromDictionary<CFStringRef>(attributes, |
| kSecAttrAccessGroup); |
| if (!attr_access_group) { |
| continue; |
| } |
| std::string rp_id_value; |
| if (!rp_id) { |
| CFStringRef sec_attr_label = |
| base::mac::GetValueFromDictionary<CFStringRef>(attributes, |
| kSecAttrLabel); |
| if (!sec_attr_label) { |
| FIDO_LOG(ERROR) << "credential with missing kSecAttrLabel_data"; |
| continue; |
| } |
| absl::optional<std::string> opt_rp_id = DecodeRpId( |
| config_.metadata_secret, base::SysCFStringRefToUTF8(sec_attr_label)); |
| if (!opt_rp_id) { |
| FIDO_LOG(ERROR) << "could not decode RP ID"; |
| continue; |
| } |
| rp_id_value = *opt_rp_id; |
| } else { |
| rp_id_value = *rp_id; |
| } |
| CFDataRef application_label = base::mac::GetValueFromDictionary<CFDataRef>( |
| attributes, kSecAttrApplicationLabel); |
| if (!application_label) { |
| FIDO_LOG(ERROR) << "credential with missing application label"; |
| return absl::nullopt; |
| } |
| std::vector<uint8_t> credential_id(CFDataGetBytePtr(application_label), |
| CFDataGetBytePtr(application_label) + |
| CFDataGetLength(application_label)); |
| if (!credential_ids.empty() && |
| !base::Contains(credential_ids, credential_id)) { |
| continue; |
| } |
| |
| // Decode `CredentialMetadata` from the `kSecAttrApplicationTag` attribute |
| // for V3 credentials, or from the credential ID for version <= 2. |
| absl::optional<CredentialMetadata> metadata; |
| CFDataRef application_tag_ref = |
| base::mac::GetValueFromDictionary<CFDataRef>(attributes, |
| kSecAttrApplicationTag); |
| // On version < 3 credentials, kSecAttrApplicationTag is a CFStringRef, |
| // which means `application_tag_ref` would be nullptr. |
| if (application_tag_ref) { |
| const base::span<const uint8_t> application_tag( |
| CFDataGetBytePtr(application_tag_ref), |
| CFDataGetBytePtr(application_tag_ref) + |
| CFDataGetLength(application_tag_ref)); |
| metadata = UnsealMetadataFromApplicationTag(config_.metadata_secret, |
| rp_id_value, application_tag); |
| } else { |
| metadata = UnsealMetadataFromLegacyCredentialId( |
| config_.metadata_secret, rp_id_value, credential_id); |
| } |
| if (!metadata) { |
| FIDO_LOG(ERROR) << "credential with invalid metadata"; |
| return absl::nullopt; |
| } |
| |
| SecKeyRef key = |
| base::mac::GetValueFromDictionary<SecKeyRef>(attributes, kSecValueRef); |
| if (!key) { |
| FIDO_LOG(ERROR) << "credential with missing value ref"; |
| return absl::nullopt; |
| } |
| base::ScopedCFTypeRef<SecKeyRef> private_key(key, |
| base::scoped_policy::RETAIN); |
| |
| credentials.emplace_back(std::move(private_key), std::move(credential_id), |
| std::move(*metadata), std::move(rp_id_value)); |
| } |
| return std::move(credentials); |
| } |
| |
| bool TouchIdCredentialStore::DeleteCredentialById( |
| base::span<const uint8_t> credential_id) const { |
| // The sane way to delete a credential would be by SecKeyRef, like so: |
| // |
| // base::ScopedCFTypeRef<CFMutableDictionaryRef> query( |
| // CFDictionaryCreateMutable(kCFAllocatorDefault, 0, |
| // &kCFTypeDictionaryKeyCallBacks, |
| // &kCFTypeDictionaryValueCallBacks)); |
| // CFDictionarySetValue(query, kSecValueRef, sec_key_ref); |
| // OSStatus status = Keychain::GetInstance().ItemDelete(query); |
| // |
| // But on macOS that looks for `sec_key_ref` in the legacy keychain instead of |
| // the "iOS" keychain that secure enclave credentials live in, and so the call |
| // fails with `errSecItemNotFound`. macOS 10.15 added |
| // `kSecUseDataProtectionKeychain` to force a query to the right keychain, but |
| // we need to support older versions of macOS for now. Hence, we must delete |
| // keychain items by credential ID (stored in `kSecAttrApplicationLabel`). |
| base::ScopedCFTypeRef<CFMutableDictionaryRef> query(CFDictionaryCreateMutable( |
| kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, |
| &kCFTypeDictionaryValueCallBacks)); |
| CFDictionarySetValue( |
| query, kSecAttrAccessGroup, |
| base::SysUTF8ToCFStringRef(config_.keychain_access_group)); |
| CFDictionarySetValue(query, kSecClass, kSecClassKey); |
| CFDictionarySetValue( |
| query, kSecAttrApplicationLabel, |
| base::apple::NSToCFPtrCast([NSData dataWithBytes:credential_id.data() |
| length:credential_id.size()])); |
| OSStatus status = Keychain::GetInstance().ItemDelete(query); |
| if (status != errSecSuccess) { |
| OSSTATUS_DLOG(ERROR, status) << "SecItemDelete failed"; |
| return false; |
| } |
| return true; |
| } |
| |
| void RecordUpdateCredentialStatus( |
| TouchIdCredentialStoreUpdateCredentialStatus update_status) { |
| base::UmaHistogramEnumeration( |
| "WebAuthentication.TouchIdCredentialStore.UpdateCredential", |
| update_status); |
| } |
| |
| bool TouchIdCredentialStore::UpdateCredential( |
| base::span<uint8_t> credential_id_span, |
| const std::string& username) { |
| std::vector<uint8_t> credential_id = |
| fido_parsing_utils::Materialize(credential_id_span); |
| |
| absl::optional<std::list<Credential>> credentials = FindCredentialsImpl( |
| /*rp_id=*/absl::nullopt, {credential_id}); |
| if (!credentials) { |
| FIDO_LOG(ERROR) << "no credentials found"; |
| RecordUpdateCredentialStatus( |
| TouchIdCredentialStoreUpdateCredentialStatus::kNoCredentialsFound); |
| return false; |
| } |
| base::ScopedCFTypeRef<CFMutableDictionaryRef> params( |
| CFDictionaryCreateMutable(kCFAllocatorDefault, 0, |
| &kCFTypeDictionaryKeyCallBacks, |
| &kCFTypeDictionaryValueCallBacks)); |
| bool found_credential = false; |
| for (Credential& credential : *credentials) { |
| if (credential.credential_id == credential_id) { |
| credential.metadata.user_name = username; |
| std::vector<uint8_t> sealed_metadata = SealCredentialMetadata( |
| config_.metadata_secret, credential.rp_id, credential.metadata); |
| CFDictionarySetValue(params, kSecAttrApplicationTag, |
| base::apple::NSToCFPtrCast([NSData |
| dataWithBytes:sealed_metadata.data() |
| length:sealed_metadata.size()])); |
| found_credential = true; |
| break; |
| } |
| } |
| if (!found_credential) { |
| FIDO_LOG(ERROR) << "no credential with matching credential_id"; |
| RecordUpdateCredentialStatus( |
| TouchIdCredentialStoreUpdateCredentialStatus::kNoMatchingCredentialId); |
| return false; |
| } |
| base::ScopedCFTypeRef<CFMutableDictionaryRef> query(CFDictionaryCreateMutable( |
| kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, |
| &kCFTypeDictionaryValueCallBacks)); |
| CFDictionarySetValue( |
| query, kSecAttrAccessGroup, |
| base::SysUTF8ToCFStringRef(config_.keychain_access_group)); |
| CFDictionarySetValue(query, kSecClass, kSecClassKey); |
| CFDictionarySetValue( |
| query, kSecAttrApplicationLabel, |
| base::apple::NSToCFPtrCast([NSData dataWithBytes:credential_id.data() |
| length:credential_id.size()])); |
| OSStatus status = Keychain::GetInstance().ItemUpdate(query, params); |
| if (status != errSecSuccess) { |
| OSSTATUS_DLOG(ERROR, status) << "SecItemUpdate failed"; |
| RecordUpdateCredentialStatus( |
| TouchIdCredentialStoreUpdateCredentialStatus::kSecItemUpdateFailure); |
| return false; |
| } |
| RecordUpdateCredentialStatus( |
| TouchIdCredentialStoreUpdateCredentialStatus::kUpdateCredentialSuccess); |
| return true; |
| } |
| |
| } // namespace device::fido::mac |