/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "KeychainSecret.h"

#include <Security/Security.h>

#include "mozilla/Logging.h"
#include "mozilla/glean/SecurityManagerSslMetrics.h"
#include "nsPrintfCString.h"

// This is the implementation of KeychainSecret, an instantiation of OSKeyStore
// for OS X. It uses the system keychain, hence the name.

using namespace mozilla;

LazyLogModule gKeychainSecretLog("keychainsecret");

KeychainSecret::KeychainSecret() {}

KeychainSecret::~KeychainSecret() {}

ScopedCFType<CFStringRef> MozillaStringToCFString(const nsACString& stringIn) {
  // https://developer.apple.com/documentation/corefoundation/1543419-cfstringcreatewithbytes
  ScopedCFType<CFStringRef> stringOut(CFStringCreateWithBytes(
      nullptr, reinterpret_cast<const UInt8*>(stringIn.BeginReading()),
      stringIn.Length(), kCFStringEncodingUTF8, false));
  return stringOut;
}

nsresult KeychainSecret::StoreSecret(const nsACString& aSecret,
                                     const nsACString& aLabel) {
  // This creates a CFDictionary of the form:
  // { class: generic password,
  //   account: the given label,
  //   value: the given secret }
  // "account" is the way we differentiate different secrets.
  // By default, secrets stored by the application (Firefox) in this way are not
  // accessible to other applications, so we shouldn't need to worry about
  // unauthorized access or namespace collisions. This will be the case as long
  // as we never set the kSecAttrAccessGroup attribute on the CFDictionary. The
  // platform enforces this restriction using the application-identifier
  // entitlement that each application bundle should have. See
  // https://developer.apple.com/documentation/security/1401659-secitemadd?language=objc#discussion

  // The keychain does not overwrite secrets by default (unlike other backends
  // like libsecret and credential manager). To be consistent, we first delete
  // any previously-stored secrets that use the given label.
  nsresult rv = DeleteSecret(aLabel);
  if (NS_FAILED(rv)) {
    MOZ_LOG(gKeychainSecretLog, LogLevel::Debug,
            ("DeleteSecret before StoreSecret failed"));
    return rv;
  }
  const CFStringRef keys[] = {kSecClass, kSecAttrAccount, kSecValueData};
  ScopedCFType<CFStringRef> label(MozillaStringToCFString(aLabel));
  if (!label) {
    MOZ_LOG(gKeychainSecretLog, LogLevel::Debug,
            ("MozillaStringToCFString failed"));
    return NS_ERROR_FAILURE;
  }
  ScopedCFType<CFDataRef> secret(CFDataCreate(
      nullptr, reinterpret_cast<const UInt8*>(aSecret.BeginReading()),
      aSecret.Length()));
  if (!secret) {
    MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, ("CFDataCreate failed"));
    return NS_ERROR_FAILURE;
  }
  const void* values[] = {kSecClassGenericPassword, label.get(), secret.get()};
  static_assert(std::size(keys) == std::size(values),
                "mismatched SecItemAdd key/value array sizes");
  ScopedCFType<CFDictionaryRef> addDictionary(CFDictionaryCreate(
      nullptr, (const void**)&keys, (const void**)&values, std::size(keys),
      &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
  // https://developer.apple.com/documentation/security/1401659-secitemadd
  OSStatus osrv = SecItemAdd(addDictionary.get(), nullptr);
  if (osrv != errSecSuccess) {
    MOZ_LOG(gKeychainSecretLog, LogLevel::Debug,
            ("SecItemAdd failed: %d", osrv));
    nsPrintfCString osrvString("%d", osrv);
    mozilla::glean::oskeystore::ReturnCodesExtra extra = {};
    extra.function = Some("StoreSecret_SecItemAdd"_ns);
    extra.result = Some(osrvString);
    glean::oskeystore::return_codes.Record(Some(extra));
    return NS_ERROR_FAILURE;
  }
  return NS_OK;
}

nsresult KeychainSecret::DeleteSecret(const nsACString& aLabel) {
  // To delete a secret, we create a CFDictionary of the form:
  // { class: generic password,
  //   account: the given label }
  // and then call SecItemDelete.
  const CFStringRef keys[] = {kSecClass, kSecAttrAccount};
  ScopedCFType<CFStringRef> label(MozillaStringToCFString(aLabel));
  if (!label) {
    MOZ_LOG(gKeychainSecretLog, LogLevel::Debug,
            ("MozillaStringToCFString failed"));
    return NS_ERROR_FAILURE;
  }
  const void* values[] = {kSecClassGenericPassword, label.get()};
  static_assert(std::size(keys) == std::size(values),
                "mismatched SecItemDelete key/value array sizes");
  ScopedCFType<CFDictionaryRef> deleteDictionary(CFDictionaryCreate(
      nullptr, (const void**)&keys, (const void**)&values, std::size(keys),
      &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
  // https://developer.apple.com/documentation/security/1395547-secitemdelete
  OSStatus osrv = SecItemDelete(deleteDictionary.get());
  if (osrv != errSecSuccess && osrv != errSecItemNotFound) {
    MOZ_LOG(gKeychainSecretLog, LogLevel::Debug,
            ("SecItemDelete failed: %d", osrv));
    mozilla::glean::oskeystore::ReturnCodesExtra extra = {};
    extra.function = Some("DeleteSecret_SecItemDelete"_ns);
    nsPrintfCString osrvString("%d", osrv);
    extra.result = Some(osrvString);
    glean::oskeystore::return_codes.Record(Some(extra));
    return NS_ERROR_FAILURE;
  }
  return NS_OK;
}

nsresult KeychainSecret::RetrieveSecret(const nsACString& aLabel,
                                        /* out */ nsACString& aSecret) {
  // To retrieve a secret, we create a CFDictionary of the form:
  // { class: generic password,
  //   account: the given label,
  //   match limit: match one,
  //   return attributes: true,
  //   return data: true }
  // This searches for and returns the attributes and data for the secret
  // matching the given label. We then extract the data (i.e. the secret) and
  // return it.
  const CFStringRef keys[] = {kSecClass, kSecAttrAccount, kSecMatchLimit,
                              kSecReturnAttributes, kSecReturnData};
  ScopedCFType<CFStringRef> label(MozillaStringToCFString(aLabel));
  if (!label) {
    MOZ_LOG(gKeychainSecretLog, LogLevel::Debug,
            ("MozillaStringToCFString failed"));
    return NS_ERROR_FAILURE;
  }
  const void* values[] = {kSecClassGenericPassword, label.get(),
                          kSecMatchLimitOne, kCFBooleanTrue, kCFBooleanTrue};
  static_assert(std::size(keys) == std::size(values),
                "mismatched SecItemCopyMatching key/value array sizes");
  ScopedCFType<CFDictionaryRef> searchDictionary(CFDictionaryCreate(
      nullptr, (const void**)&keys, (const void**)&values, std::size(keys),
      &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
  CFTypeRef item;
  // https://developer.apple.com/documentation/security/1398306-secitemcopymatching
  OSStatus osrv = SecItemCopyMatching(searchDictionary.get(), &item);
  if (osrv == errSecItemNotFound) {
    MOZ_LOG(gKeychainSecretLog, LogLevel::Debug,
            ("Key not found in key store"));
    return NS_ERROR_NOT_AVAILABLE;
  }
  if (osrv != errSecSuccess) {
    MOZ_LOG(gKeychainSecretLog, LogLevel::Debug,
            ("SecItemCopyMatching failed: %d", osrv));
    mozilla::glean::oskeystore::ReturnCodesExtra extra = {};
    extra.function = Some("RetrieveSecret_SecItemCopyMatching"_ns);
    nsPrintfCString osrvString("%d", osrv);
    extra.result = Some(osrvString);
    glean::oskeystore::return_codes.Record(Some(extra));
    return NS_ERROR_FAILURE;
  }
  ScopedCFType<CFDictionaryRef> dictionary(
      reinterpret_cast<CFDictionaryRef>(item));
  CFDataRef secret = reinterpret_cast<CFDataRef>(
      CFDictionaryGetValue(dictionary.get(), kSecValueData));
  if (!secret) {
    MOZ_LOG(gKeychainSecretLog, LogLevel::Debug,
            ("CFDictionaryGetValue failed"));
    return NS_ERROR_FAILURE;
  }
  aSecret.Assign(reinterpret_cast<const char*>(CFDataGetBytePtr(secret)),
                 CFDataGetLength(secret));
  return NS_OK;
}
