// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "crypto/apple/fake_keychain_v2.h"

#import <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>
#import <Security/Security.h>

#include <algorithm>
#include <vector>

#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/apple/scoped_typeref.h"
#include "base/check_op.h"
#include "base/containers/to_vector.h"
#include "base/memory/scoped_policy.h"
#include "base/metrics/histogram_macros.h"
#include "base/notimplemented.h"
#include "base/strings/sys_string_conversions.h"
#include "base/time/time.h"
#include "crypto/apple/keychain_v2.h"

#if !BUILDFLAG(IS_IOS_TVOS)
#import <LocalAuthentication/LocalAuthentication.h>
#endif

#if defined(LEAK_SANITIZER)
#include <sanitizer/lsan_interface.h>
#endif

namespace crypto::apple {

namespace {

// Returns true if the `item_value` matches the `query_value`.
// A null `query_value` is a wildcard and is always considered a match.
bool Matches(CFTypeRef query_value, CFTypeRef item_value) {
  return !query_value || (item_value && CFEqual(query_value, item_value));
}

constexpr char kPassword[] = "mock_password";

// Adds an entry to a local histogram to indicate that the Keychain would have
// been accessed, if this class were not a mock of the Keychain.
void IncrementKeychainAccessHistogram() {
  // This local histogram is accessed by Telemetry to track the number of times
  // the keychain is accessed, since keychain access is known to be synchronous
  // and slow.
  LOCAL_HISTOGRAM_BOOLEAN("OSX.Keychain.Access", true);
}

}  // namespace

FakeKeychainV2::FakeKeychainV2(const std::string& keychain_access_group)
    : keychain_access_group_(
          base::SysUTF8ToCFStringRef(keychain_access_group)) {}
FakeKeychainV2::~FakeKeychainV2() {
  // Avoid shutdown leak of error string in Security.framework.
  // See
  // https://github.com/apple-oss-distributions/Security/blob/Security-60158.140.3/OSX/libsecurity_keychain/lib/SecBase.cpp#L88
#if defined(LEAK_SANITIZER)
  __lsan_do_leak_check();
#endif
}

NSArray* FakeKeychainV2::GetTokenIDs() {
  if (is_secure_enclave_available_) {
    return @[ base::apple::CFToNSPtrCast(kSecAttrTokenIDSecureEnclave) ];
  }
  return @[];
}

base::apple::ScopedCFTypeRef<SecKeyRef> FakeKeychainV2::KeyCreateRandomKey(
    CFDictionaryRef params,
    CFErrorRef* error) {
  // Validate certain fields that we always expect to be set.
  DCHECK(
      base::apple::GetValueFromDictionary<CFStringRef>(params, kSecAttrLabel));
  // kSecAttrApplicationTag is CFDataRef for new credentials and CFStringRef for
  // version < 3. Keychain docs say it should be CFDataRef
  // (https://developer.apple.com/documentation/security/ksecattrapplicationtag).
  CFTypeRef application_tag = nil;
  CFDictionaryGetValueIfPresent(params, kSecAttrApplicationTag,
                                &application_tag);
  if (application_tag) {
    CHECK(base::apple::CFCast<CFDataRef>(application_tag) ||
          base::apple::CFCast<CFStringRef>(application_tag));
  }
  DCHECK_EQ(
      base::apple::GetValueFromDictionary<CFStringRef>(params, kSecAttrTokenID),
      kSecAttrTokenIDSecureEnclave);
  DCHECK(CFEqual(base::apple::GetValueFromDictionary<CFStringRef>(
                     params, kSecAttrAccessGroup),
                 keychain_access_group_.get()));

  // Call Keychain services to create a key pair, but first drop all parameters
  // that aren't appropriate in tests.
  base::apple::ScopedCFTypeRef<CFMutableDictionaryRef> params_copy(
      CFDictionaryCreateMutableCopy(kCFAllocatorDefault, /*capacity=*/0,
                                    params));
  // Don't create a Secure Enclave key.
  CFDictionaryRemoveValue(params_copy.get(), kSecAttrTokenID);
  // Don't bind to a keychain-access-group, which would require an entitlement.
  CFDictionaryRemoveValue(params_copy.get(), kSecAttrAccessGroup);

  base::apple::ScopedCFTypeRef<CFMutableDictionaryRef> private_key_params(
      CFDictionaryCreateMutableCopy(
          kCFAllocatorDefault, /*capacity=*/0,
          base::apple::GetValueFromDictionary<CFDictionaryRef>(
              params_copy.get(), kSecPrivateKeyAttrs)));
  DCHECK(CFEqual(base::apple::GetValueFromDictionary<CFBooleanRef>(
                     private_key_params.get(), kSecAttrIsPermanent),
                 kCFBooleanTrue));
  CFDictionarySetValue(private_key_params.get(), kSecAttrIsPermanent,
                       kCFBooleanFalse);
  CFDictionaryRemoveValue(private_key_params.get(), kSecAttrAccessControl);
  CFDictionaryRemoveValue(private_key_params.get(),
                          kSecUseAuthenticationContext);
  CFDictionarySetValue(params_copy.get(), kSecPrivateKeyAttrs,
                       private_key_params.get());
  base::apple::ScopedCFTypeRef<SecKeyRef> private_key(
      SecKeyCreateRandomKey(params_copy.get(), error));
  if (!private_key) {
    return base::apple::ScopedCFTypeRef<SecKeyRef>();
  }

  // Stash everything in `items_` so it can be  retrieved in with
  // `ItemCopyMatching. This uses the original `params` rather than the modified
  // copy so that `ItemCopyMatching()` will correctly filter on
  // kSecAttrAccessGroup.
  base::apple::ScopedCFTypeRef<CFMutableDictionaryRef> keychain_item(
      CFDictionaryCreateMutableCopy(kCFAllocatorDefault, /*capacity=*/0,
                                    params));
  CFDictionarySetValue(keychain_item.get(), kSecValueRef, private_key.get());

  // When left unset, the real keychain sets the application label to the hash
  // of the public key on creation. We need to retrieve it to allow filtering
  // for it later.
  if (!base::apple::GetValueFromDictionary<CFDataRef>(
          keychain_item.get(), kSecAttrApplicationLabel)) {
    base::apple::ScopedCFTypeRef<CFDictionaryRef> key_metadata(
        SecKeyCopyAttributes(private_key.get()));
    CFDataRef application_label =
        base::apple::GetValueFromDictionary<CFDataRef>(
            key_metadata.get(), kSecAttrApplicationLabel);
    CFDictionarySetValue(keychain_item.get(), kSecAttrApplicationLabel,
                         application_label);
  }

  CFDateRef unix_epoch =
      base::apple::NSToCFPtrCast(base::Time::UnixEpoch().ToNSDate());
  CFDictionarySetValue(keychain_item.get(), kSecAttrCreationDate, unix_epoch);
  CFDictionarySetValue(keychain_item.get(), kSecAttrModificationDate,
                       unix_epoch);

  items_.push_back(keychain_item);

  return private_key;
}

base::apple::ScopedCFTypeRef<CFDictionaryRef> FakeKeychainV2::KeyCopyAttributes(
    SecKeyRef key) {
  const auto& it = std::ranges::find_if(items_, [&key](const auto& item) {
    return CFEqual(key, CFDictionaryGetValue(item.get(), kSecValueRef));
  });
  if (it == items_.end()) {
    return base::apple::ScopedCFTypeRef<CFDictionaryRef>();
  }
  base::apple::ScopedCFTypeRef<CFMutableDictionaryRef> result(
      CFDictionaryCreateMutableCopy(kCFAllocatorDefault, /*capacity=*/0,
                                    it->get()));
  // The real implementation does not return the actual key.
  CFDictionaryRemoveValue(result.get(), kSecValueRef);
  return result;
}

OSStatus FakeKeychainV2::ItemAdd(CFDictionaryRef attributes,
                                 CFTypeRef* result) {
  CFStringRef keychain_access_group =
      base::apple::GetValueFromDictionary<CFStringRef>(attributes,
                                                       kSecAttrAccessGroup);
  if (!CFEqual(keychain_access_group, keychain_access_group_.get())) {
    return errSecMissingEntitlement;
  }
  base::apple::ScopedCFTypeRef<CFDictionaryRef> item(
      attributes, base::scoped_policy::RETAIN);
  items_.push_back(item);
  return errSecSuccess;
}

OSStatus FakeKeychainV2::ItemCopyMatching(CFDictionaryRef query,
                                          CFTypeRef* result) {
  CFStringRef match_limit =
      base::apple::GetValueFromDictionary<CFStringRef>(query, kSecMatchLimit);
  bool match_all = match_limit && CFEqual(match_limit, kSecMatchLimitAll);

  // Match fields present in `query`.
  CFStringRef query_label =
      base::apple::GetValueFromDictionary<CFStringRef>(query, kSecAttrLabel);
  CFDataRef query_application_label =
      base::apple::GetValueFromDictionary<CFDataRef>(query,
                                                     kSecAttrApplicationLabel);
  // kSecAttrApplicationTag can be CFStringRef for legacy credentials and
  // CFDataRef for new ones, hence using CFTypeRef.
  CFTypeRef query_application_tag =
      CFDictionaryGetValue(query, kSecAttrApplicationTag);

  CFStringRef query_attr_service =
      base::apple::GetValueFromDictionary<CFStringRef>(query, kSecAttrService);

  CFStringRef query_attr_accessible =
      base::apple::GetValueFromDictionary<CFStringRef>(query,
                                                       kSecAttrAccessible);

  // Filter the items based on `query`.
  base::apple::ScopedCFTypeRef<CFMutableArrayRef> items(
      CFArrayCreateMutable(nullptr, items_.size(), &kCFTypeArrayCallBacks));
  for (auto& item : items_) {
    // Each `Keychain` instance is expected to operate only on items of a single
    // keychain-access-group, which is tied to the `Profile`.
    CFStringRef query_access_group =
        base::apple::GetValueFromDictionary<CFStringRef>(query,
                                                         kSecAttrAccessGroup);
    if (query_access_group) {
      DCHECK(CFEqual(query_access_group,
                     base::apple::GetValueFromDictionary<CFStringRef>(
                         item.get(), kSecAttrAccessGroup)) &&
             CFEqual(query_access_group, keychain_access_group_.get()));
    } else {
      // If no access group is specified in the query, we only return items
      // belonging to this Keychain instance's access group.
      if (!CFEqual(keychain_access_group_.get(),
                   base::apple::GetValueFromDictionary<CFStringRef>(
                       item.get(), kSecAttrAccessGroup))) {
        continue;
      }
    }

    CFStringRef item_label = base::apple::GetValueFromDictionary<CFStringRef>(
        item.get(), kSecAttrLabel);
    CFDataRef item_application_label =
        base::apple::GetValueFromDictionary<CFDataRef>(
            item.get(), kSecAttrApplicationLabel);
    CFTypeRef item_application_tag =
        CFDictionaryGetValue(item.get(), kSecAttrApplicationTag);
    CFStringRef item_attr_service =
        base::apple::GetValueFromDictionary<CFStringRef>(item.get(),
                                                         kSecAttrService);
    CFStringRef item_attr_accessible =
        base::apple::GetValueFromDictionary<CFStringRef>(item.get(),
                                                         kSecAttrAccessible);
    if (!Matches(query_label, item_label) ||
        !Matches(query_application_label, item_application_label) ||
        !Matches(query_application_tag, item_application_tag) ||
        !Matches(query_attr_service, item_attr_service) ||
        !Matches(query_attr_accessible, item_attr_accessible)) {
      continue;
    }
    if (match_all) {
      base::apple::ScopedCFTypeRef<CFDictionaryRef> item_copy(
          CFDictionaryCreateCopy(kCFAllocatorDefault, item.get()));
      CFArrayAppendValue(items.get(), item_copy.get());
    } else {
      if (result) {
        *result = CFDictionaryCreateCopy(kCFAllocatorDefault, item.get());
      }
      return errSecSuccess;
    }
  }
  if (CFArrayGetCount(items.get()) == 0) {
    return errSecItemNotFound;
  }
  if (result) {
    *result = items.release();
  }
  return errSecSuccess;
}

OSStatus FakeKeychainV2::ItemDelete(CFDictionaryRef query) {
  // Validate certain fields that we always expect to be set.
  DCHECK_EQ(base::apple::GetValueFromDictionary<CFStringRef>(query, kSecClass),
            kSecClassKey);
  CHECK(CFEqual(base::apple::GetValueFromDictionary<CFStringRef>(
                    query, kSecAttrAccessGroup),
                keychain_access_group_.get()));
  CFDataRef query_application_label =
      base::apple::GetValueFromDictionary<CFDataRef>(query,
                                                     kSecAttrApplicationLabel);
  CHECK(query_application_label);
  // kSecAttrApplicationTag can be CFStringRef for legacy credentials and
  // CFDataRef for new ones, hence using CFTypeRef.
  CFTypeRef query_application_tag =
      CFDictionaryGetValue(query, kSecAttrApplicationTag);
  const size_t n_erased = std::erase_if(
      items_, [&](const base::apple::ScopedCFTypeRef<CFDictionaryRef>& item) {
        CFDataRef item_application_label =
            base::apple::GetValueFromDictionary<CFDataRef>(
                item.get(), kSecAttrApplicationLabel);
        CHECK(item_application_label);
        CFTypeRef item_application_tag =
            CFDictionaryGetValue(item.get(), kSecAttrApplicationTag);
        return CFEqual(query_application_label, item_application_label) &&
               Matches(query_application_tag, item_application_tag);
      });

  return n_erased != 0 ? errSecSuccess : errSecItemNotFound;
}

OSStatus FakeKeychainV2::ItemUpdate(CFDictionaryRef query,
                                    CFDictionaryRef attributes_to_update) {
  if (item_update_result_ != noErr) {
    return item_update_result_;
  }
  CFStringRef query_class =
      base::apple::GetValueFromDictionary<CFStringRef>(query, kSecClass);
  DCHECK(CFEqual(query_class, kSecClassKey) ||
         CFEqual(query_class, kSecClassGenericPassword));
  DCHECK(CFEqual(base::apple::GetValueFromDictionary<CFStringRef>(
                     query, kSecAttrAccessGroup),
                 keychain_access_group_.get()));

  CFDataRef query_application_label =
      base::apple::GetValueFromDictionary<CFDataRef>(query,
                                                     kSecAttrApplicationLabel);
  CFStringRef query_account =
      base::apple::GetValueFromDictionary<CFStringRef>(query, kSecAttrAccount);

  std::vector<base::apple::ScopedCFTypeRef<CFDictionaryRef>> new_items;
  OSStatus result = errSecItemNotFound;
  for (base::apple::ScopedCFTypeRef<CFDictionaryRef>& item : items_) {
    if (query_application_label) {
      CFDataRef item_application_label =
          base::apple::GetValueFromDictionary<CFDataRef>(
              item.get(), kSecAttrApplicationLabel);
      if (!item_application_label ||
          !CFEqual(query_application_label, item_application_label)) {
        new_items.push_back(item);
        continue;
      }
    }
    if (query_account) {
      CFStringRef item_account =
          base::apple::GetValueFromDictionary<CFStringRef>(item.get(),
                                                           kSecAttrAccount);
      if (!item_account || !CFEqual(query_account, item_account)) {
        new_items.push_back(item);
        continue;
      }
    }

    base::apple::ScopedCFTypeRef<CFMutableDictionaryRef> item_copy(
        CFDictionaryCreateMutableCopy(kCFAllocatorDefault, /*capacity=*/0,
                                      item.get()));
    [base::apple::CFToNSPtrCast(item_copy.get())
        addEntriesFromDictionary:base::apple::CFToNSPtrCast(
                                     attributes_to_update)];
    new_items.push_back(item_copy);
    // Succeed if we replaced at least one item matching the query.
    result = errSecSuccess;
  }

  items_ = new_items;
  return result;
}

base::expected<std::vector<uint8_t>, OSStatus>
FakeKeychainV2::FindGenericPassword(std::string_view service_name,
                                    std::string_view account_name) {
  IncrementKeychainAccessHistogram();

  // When simulating |noErr|, return mock password. Otherwise, return given
  // code.
  if (find_generic_result_ == noErr) {
    return base::ToVector(base::byte_span_from_cstring(kPassword));
  }

  return base::unexpected(find_generic_result_);
}

OSStatus FakeKeychainV2::AddGenericPassword(
    std::string_view service_name,
    std::string_view account_name,
    base::span<const uint8_t> password) {
  IncrementKeychainAccessHistogram();
  called_add_generic_ = true;

  DCHECK(!password.empty());
  return noErr;
}

std::string FakeKeychainV2::GetEncryptionPassword() const {
  IncrementKeychainAccessHistogram();
  return kPassword;
}

#if !BUILDFLAG(IS_IOS)
base::apple::ScopedCFTypeRef<CFTypeRef>
FakeKeychainV2::TaskCopyValueForEntitlement(SecTaskRef task,
                                            CFStringRef entitlement,
                                            CFErrorRef* error) {
  CHECK(task);
  CHECK(CFEqual(entitlement,
                base::SysUTF8ToCFStringRef("keychain-access-groups").get()))
      << "Entitlement " << entitlement << " not supported by fake";
  base::apple::ScopedCFTypeRef<CFMutableArrayRef> keychain_access_groups(
      CFArrayCreateMutable(kCFAllocatorDefault, /*capacity=*/1,
                           &kCFTypeArrayCallBacks));
  CFArrayAppendValue(keychain_access_groups.get(),
                     keychain_access_group_.get());
  return keychain_access_groups;
}
#endif  // !BUILDFLAG(IS_IOS)

#if !BUILDFLAG(IS_IOS_TVOS)
BOOL FakeKeychainV2::LAContextCanEvaluatePolicy(
    LAPolicy policy,
    NSError* __autoreleasing* error) {
  switch (policy) {
    case LAPolicyDeviceOwnerAuthentication:
      return uv_method_ == UVMethod::kBiometrics ||
             uv_method_ == UVMethod::kPasswordOnly;
    case LAPolicyDeviceOwnerAuthenticationWithBiometrics:
      return uv_method_ == UVMethod::kBiometrics;
#if !BUILDFLAG(IS_IOS)
    case LAPolicyDeviceOwnerAuthenticationWithBiometricsOrCompanion:
      return uv_method_ == UVMethod::kBiometrics;
#endif        // !BUILDFLAG(IS_IOS)
    default:  // Avoid needing to refer to values not available in the minimum
              // supported macOS version.
      NOTIMPLEMENTED();
      return false;
  }
}
#endif  // !BUILDFLAG(IS_IOS_TVOS)

}  // namespace crypto::apple
