blob: 19f5749419d9191f15c7b9fe4d4a6a8c0613b227 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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 <string>
#import <Foundation/Foundation.h>
#import <Security/Security.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/optional.h"
#include "base/stl_util.h"
#include "base/strings/sys_string_conversions.h"
#include "device/base/features.h"
#include "device/fido/mac/credential_metadata.h"
#include "device/fido/mac/keychain.h"
namespace device {
namespace fido {
namespace mac {
namespace {
// 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;
});
}
base::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)
API_AVAILABLE(macosx(10.12.2)) {
// 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, nullptr, nullptr));
CFDictionarySetValue(query, kSecClass, kSecClassKey);
CFDictionarySetValue(query, kSecAttrAccessGroup,
base::SysUTF8ToNSString(keychain_access_group));
CFDictionarySetValue(query, kSecMatchLimit, kSecMatchLimitAll);
// Return the key reference and its attributes.
CFDictionarySetValue(query, kSecReturnRef, @YES);
CFDictionarySetValue(query, kSecReturnAttributes, @YES);
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 base::nullopt;
}
if (status != errSecSuccess) {
OSSTATUS_DLOG(ERROR, status) << "SecItemCopyMatching failed";
return base::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 base::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;
}
base::Optional<std::string> opt_rp_id = CredentialMetadata::DecodeRpId(
metadata_secret, base::SysCFStringRefToUTF8(sec_attr_label));
if (!opt_rp_id) {
DVLOG(1) << "key doesn't belong to this profile";
continue;
}
result.push_back(base::ScopedCFTypeRef<CFDictionaryRef>(
attributes, base::scoped_policy::RETAIN));
}
FilterKeychainItemsByCreationDate(&result, created_not_before,
created_not_after);
return result;
}
bool DoDeleteWebAuthnCredentials(const std::string& keychain_access_group,
const std::string& metadata_secret,
base::Time created_not_before,
base::Time created_not_after)
API_AVAILABLE(macosx(10.12.2)) {
bool result = true;
base::Optional<std::vector<base::ScopedCFTypeRef<CFDictionaryRef>>>
keychain_items =
QueryKeychainItemsForProfile(keychain_access_group, metadata_secret,
created_not_before, created_not_after);
if (!keychain_items) {
return false;
}
// The sane way to delete this item would be to build a query that has the
// kSecMatchItemList field set to a list of SecKeyRef objects that need
// deleting. Sadly, on macOS that appears to work only if you also set
// kSecAttrNoLegacy (which is an internal symbol); otherwise it appears to
// only search the "legacy" keychain and return errSecItemNotFound. What
// does work however, is to look up and delete by the (unique)
// kSecAttrApplicationLabel (which stores the credential id). So we clumsily
// do this for each item instead.
for (const CFDictionaryRef& attributes : *keychain_items) {
CFDataRef sec_attr_app_label = base::mac::GetValueFromDictionary<CFDataRef>(
attributes, kSecAttrApplicationLabel);
if (!sec_attr_app_label) {
DLOG(ERROR) << "missing application label";
continue;
}
base::ScopedCFTypeRef<CFMutableDictionaryRef> delete_query(
CFDictionaryCreateMutable(kCFAllocatorDefault, 0, nullptr, nullptr));
CFDictionarySetValue(delete_query, kSecClass, kSecClassKey);
CFDictionarySetValue(delete_query, kSecAttrApplicationLabel,
sec_attr_app_label);
OSStatus status = Keychain::GetInstance().ItemDelete(delete_query);
if (status != errSecSuccess) {
OSSTATUS_DLOG(ERROR, status) << "SecItemDelete failed";
result = false;
continue;
}
}
return result;
}
size_t DoCountWebAuthnCredentials(const std::string& keychain_access_group,
const std::string& metadata_secret,
base::Time created_not_before,
base::Time created_not_after)
API_AVAILABLE(macosx(10.12.2)) {
base::Optional<std::vector<base::ScopedCFTypeRef<CFDictionaryRef>>>
keychain_items =
QueryKeychainItemsForProfile(keychain_access_group, 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();
}
} // namespace
TouchIdCredentialStore::TouchIdCredentialStore(AuthenticatorConfig config)
: config_(std::move(config)) {}
TouchIdCredentialStore::~TouchIdCredentialStore() = default;
bool TouchIdCredentialStore::DeleteCredentials(base::Time created_not_before,
base::Time created_not_after) {
if (base::FeatureList::IsEnabled(device::kWebAuthTouchId)) {
// Touch ID uses macOS APIs available in 10.12.2 or newer. No need to check
// for credentials in lower OS versions.
if (__builtin_available(macos 10.12.2, *)) {
return DoDeleteWebAuthnCredentials(config_.keychain_access_group,
config_.metadata_secret,
created_not_before, created_not_after);
}
}
return true;
}
size_t TouchIdCredentialStore::CountCredentials(base::Time created_not_before,
base::Time created_not_after) {
if (base::FeatureList::IsEnabled(device::kWebAuthTouchId)) {
if (__builtin_available(macos 10.12.2, *)) {
return DoCountWebAuthnCredentials(config_.keychain_access_group,
config_.metadata_secret,
created_not_before, created_not_after);
}
}
return 0;
}
} // namespace mac
} // namespace fido
} // namespace device