/*
 * 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 "nsClientAuthRemember.h"

#include "mozilla/BasePrincipal.h"
#include "mozilla/RefPtr.h"
#include "nsCRT.h"
#include "nsINSSComponent.h"
#include "nsPrintfCString.h"
#include "nsNSSComponent.h"
#include "nsIDataStorage.h"
#include "nsIObserverService.h"
#include "nsNetUtil.h"
#include "nsPromiseFlatString.h"
#include "nsThreadUtils.h"
#include "cert.h"
#include "nspr.h"
#include "pk11pub.h"
#include "certdb.h"
#include "sechash.h"

#include "nsJSUtils.h"

#ifdef XP_MACOSX
#  include <CoreFoundation/CoreFoundation.h>
#  include <Security/Security.h>
#  include "KeychainSecret.h"  // for ScopedCFType
#endif                         // XP_MACOSX

using namespace mozilla;
using namespace mozilla::psm;

NS_IMPL_ISUPPORTS(nsClientAuthRememberService, nsIClientAuthRememberService)
NS_IMPL_ISUPPORTS(nsClientAuthRemember, nsIClientAuthRememberRecord)

NS_IMETHODIMP
nsClientAuthRemember::GetAsciiHost(/*out*/ nsACString& aAsciiHost) {
  aAsciiHost = mAsciiHost;
  return NS_OK;
}

NS_IMETHODIMP
nsClientAuthRemember::GetDbKey(/*out*/ nsACString& aDBKey) {
  aDBKey = mDBKey;
  return NS_OK;
}

NS_IMETHODIMP
nsClientAuthRemember::GetEntryKey(/*out*/ nsACString& aEntryKey) {
  aEntryKey.Assign(mAsciiHost);
  aEntryKey.Append(',');
  // This used to include the SHA-256 hash of the server certificate.
  aEntryKey.Append(',');
  aEntryKey.Append(mOriginAttributesSuffix);
  return NS_OK;
}

nsresult nsClientAuthRememberService::Init() {
  if (!NS_IsMainThread()) {
    NS_ERROR("nsClientAuthRememberService::Init called off the main thread");
    return NS_ERROR_NOT_SAME_THREAD;
  }

  nsCOMPtr<nsIDataStorageManager> dataStorageManager(
      do_GetService("@mozilla.org/security/datastoragemanager;1"));
  if (!dataStorageManager) {
    return NS_ERROR_FAILURE;
  }
  nsresult rv =
      dataStorageManager->Get(nsIDataStorageManager::ClientAuthRememberList,
                              getter_AddRefs(mClientAuthRememberList));
  if (NS_FAILED(rv)) {
    return rv;
  }
  if (!mClientAuthRememberList) {
    return NS_ERROR_FAILURE;
  }

  return NS_OK;
}

NS_IMETHODIMP
nsClientAuthRememberService::ForgetRememberedDecision(const nsACString& key) {
  nsresult rv = mClientAuthRememberList->Remove(
      PromiseFlatCString(key), nsIDataStorage::DataType::Persistent);
  if (NS_FAILED(rv)) {
    return rv;
  }
  rv = mClientAuthRememberList->Remove(PromiseFlatCString(key),
                                       nsIDataStorage::DataType::Temporary);
  if (NS_FAILED(rv)) {
    return rv;
  }
  nsCOMPtr<nsINSSComponent> nssComponent(do_GetService(NS_NSSCOMPONENT_CID));
  if (!nssComponent) {
    return NS_ERROR_NOT_AVAILABLE;
  }
  return nssComponent->ClearSSLExternalAndInternalSessionCache();
}

NS_IMETHODIMP
nsClientAuthRememberService::GetDecisions(
    nsTArray<RefPtr<nsIClientAuthRememberRecord>>& results) {
  nsTArray<RefPtr<nsIDataStorageItem>> decisions;
  nsresult rv = mClientAuthRememberList->GetAll(decisions);
  if (NS_FAILED(rv)) {
    return rv;
  }

  for (const auto& decision : decisions) {
    nsIDataStorage::DataType type;
    rv = decision->GetType(&type);
    if (NS_FAILED(rv)) {
      return rv;
    }
    if (type == nsIDataStorage::DataType::Persistent ||
        type == nsIDataStorage::DataType::Temporary) {
      nsAutoCString key;
      rv = decision->GetKey(key);
      if (NS_FAILED(rv)) {
        return rv;
      }
      nsAutoCString value;
      rv = decision->GetValue(value);
      if (NS_FAILED(rv)) {
        return rv;
      }
      RefPtr tmp = MakeRefPtr<nsClientAuthRemember>(key, value);
      results.AppendElement(std::move(tmp));
    }
  }

  return NS_OK;
}

NS_IMETHODIMP
nsClientAuthRememberService::ClearRememberedDecisions() {
  nsresult rv = mClientAuthRememberList->Clear();
  if (NS_FAILED(rv)) {
    return rv;
  }
  nsCOMPtr<nsINSSComponent> nssComponent(do_GetService(NS_NSSCOMPONENT_CID));
  if (!nssComponent) {
    return NS_ERROR_NOT_AVAILABLE;
  }
  return nssComponent->ClearSSLExternalAndInternalSessionCache();
}

NS_IMETHODIMP
nsClientAuthRememberService::DeleteDecisionsByHost(
    const nsACString& aHostName, JS::Handle<JS::Value> aOriginAttributes,
    JSContext* aCx) {
  OriginAttributes attrs;
  if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) {
    return NS_ERROR_INVALID_ARG;
  }
  bool isPrivateContext = attrs.IsPrivateBrowsing();

  nsTArray<RefPtr<nsIDataStorageItem>> decisions;
  nsresult rv = mClientAuthRememberList->GetAll(decisions);
  if (NS_FAILED(rv)) {
    return rv;
  }

  for (const auto& decision : decisions) {
    nsIDataStorage::DataType type;
    nsresult rv = decision->GetType(&type);
    if (NS_FAILED(rv)) {
      return rv;
    }
    bool isPrivateDecision = type == nsIDataStorage::DataType::Private;
    if (isPrivateContext == isPrivateDecision) {
      nsAutoCString key;
      rv = decision->GetKey(key);
      if (NS_FAILED(rv)) {
        return rv;
      }
      nsAutoCString value;
      rv = decision->GetValue(value);
      if (NS_FAILED(rv)) {
        return rv;
      }
      RefPtr tmp = MakeRefPtr<nsClientAuthRemember>(key, value);
      nsAutoCString asciiHost;
      tmp->GetAsciiHost(asciiHost);
      if (asciiHost.Equals(aHostName)) {
        rv = mClientAuthRememberList->Remove(key, type);
        if (NS_FAILED(rv)) {
          return rv;
        }
      }
    }
  }
  nsCOMPtr<nsINSSComponent> nssComponent(do_GetService(NS_NSSCOMPONENT_CID));
  if (!nssComponent) {
    return NS_ERROR_NOT_AVAILABLE;
  }
  return nssComponent->ClearSSLExternalAndInternalSessionCache();
}

NS_IMETHODIMP
nsClientAuthRememberService::RememberDecisionScriptable(
    const nsACString& aHostName, JS::Handle<JS::Value> aOriginAttributes,
    nsIX509Cert* aClientCert, Duration aDuration, JSContext* aCx) {
  OriginAttributes attrs;
  if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) {
    return NS_ERROR_INVALID_ARG;
  }
  return RememberDecision(aHostName, attrs, aClientCert, aDuration);
}

NS_IMETHODIMP
nsClientAuthRememberService::RememberDecision(
    const nsACString& aHostName, const OriginAttributes& aOriginAttributes,
    nsIX509Cert* aClientCert, Duration aDuration) {
  if (aHostName.IsEmpty()) {
    return NS_ERROR_INVALID_ARG;
  }

  // If a decision is to only be used once, it doesn't need to be remembered in
  // any way.
  if (aDuration == nsIClientAuthRememberService::Duration::Once) {
    return NS_OK;
  }

  // aClientCert == nullptr means: remember that user does not want to use a
  // cert
  if (aClientCert) {
    nsAutoCString dbkey;
    nsresult rv = aClientCert->GetDbKey(dbkey);
    if (NS_FAILED(rv)) {
      return rv;
    }
    return AddEntryToList(aHostName, aOriginAttributes, dbkey, aDuration);
  }
  return AddEntryToList(aHostName, aOriginAttributes,
                        nsClientAuthRemember::SentinelValue, aDuration);
}

#ifdef XP_MACOSX
// On macOS, users can add "identity preference" items in the keychain. These
// can be added via the Keychain Access tool. These specify mappings from
// URLs/wildcards like "*.mozilla.org" to specific client certificates. This
// function retrieves the preferred client certificate for a hostname by
// querying a system API that checks for these identity preferences.
nsresult CheckForPreferredCertificate(const nsACString& aHostName,
                                      nsACString& aCertDBKey) {
  aCertDBKey.Truncate();
  // SecIdentityCopyPreferred seems to expect a proper URI which it can use
  // for prefix and wildcard matches.
  // We don't have the full URL but we can turn the hostname into a URI with
  // an authority section, so that it matches against macOS identity preferences
  // like `*.foo.com`. If we know that this connection is always going to be
  // https, then we should put that in the URI as well, so that it matches
  // identity preferences like `https://foo.com/` as well. If we can plumb
  // the path or the full URL into this function we could also match identity
  // preferences like `https://foo.com/bar/` but for now we cannot.
  nsPrintfCString fakeUrl("//%s/", PromiseFlatCString(aHostName).get());
  ScopedCFType<CFStringRef> host(::CFStringCreateWithCString(
      kCFAllocatorDefault, fakeUrl.get(), kCFStringEncodingUTF8));
  if (!host) {
    return NS_ERROR_UNEXPECTED;
  }
  ScopedCFType<SecIdentityRef> identity(
      ::SecIdentityCopyPreferred(host.get(), nullptr, nullptr));
  if (!identity) {
    // No preferred identity for this hostname, leave aCertDBKey empty and
    // return
    return NS_OK;
  }
  SecCertificateRef certRefRaw = nullptr;
  OSStatus copyResult =
      ::SecIdentityCopyCertificate(identity.get(), &certRefRaw);
  ScopedCFType<SecCertificateRef> certRef(certRefRaw);
  if (copyResult != errSecSuccess || certRef.get() == nullptr) {
    return NS_ERROR_UNEXPECTED;
  }
  ScopedCFType<CFDataRef> der(::SecCertificateCopyData(certRef.get()));
  if (!der) {
    return NS_ERROR_UNEXPECTED;
  }

  nsTArray<uint8_t> derArray(::CFDataGetBytePtr(der.get()),
                             ::CFDataGetLength(der.get()));
  nsCOMPtr<nsIX509Cert> cert(new nsNSSCertificate(std::move(derArray)));
  return cert->GetDbKey(aCertDBKey);
}
#endif

void nsClientAuthRememberService::Migrate() {
  auto migrated = mMigrated.Lock();
  if (*migrated) {
    return;
  }
  *migrated = true;

  nsTArray<RefPtr<nsIDataStorageItem>> decisions;
  nsresult rv = mClientAuthRememberList->GetAll(decisions);
  if (NS_FAILED(rv)) {
    return;
  }
  for (const auto& decision : decisions) {
    nsIDataStorage::DataType type;
    if (NS_FAILED(decision->GetType(&type))) {
      continue;
    }
    if (type != nsIDataStorage::DataType::Persistent) {
      continue;
    }
    nsAutoCString key;
    if (NS_FAILED(decision->GetKey(key))) {
      continue;
    }
    nsAutoCString value;
    if (NS_FAILED(decision->GetValue(value))) {
      continue;
    }
    RefPtr<nsClientAuthRemember> entry(new nsClientAuthRemember(key, value));
    nsAutoCString newKey;
    if (NS_FAILED(entry->GetEntryKey(newKey))) {
      continue;
    }
    if (newKey != key) {
      if (NS_FAILED(mClientAuthRememberList->Remove(
              key, nsIDataStorage::DataType::Persistent))) {
        continue;
      }
      if (NS_FAILED(mClientAuthRememberList->Put(
              newKey, value, nsIDataStorage::DataType::Persistent))) {
        continue;
      }
    }
  }
}

NS_IMETHODIMP
nsClientAuthRememberService::HasRememberedDecision(
    const nsACString& aHostName, const OriginAttributes& aOriginAttributes,
    nsACString& aCertDBKey, bool* aRetVal) {
  NS_ENSURE_ARG_POINTER(aRetVal);
  if (aHostName.IsEmpty()) {
    return NS_ERROR_INVALID_ARG;
  }

  *aRetVal = false;
  aCertDBKey.Truncate();

  Migrate();

  nsAutoCString entryKey;
  RefPtr<nsClientAuthRemember> entry(
      new nsClientAuthRemember(aHostName, aOriginAttributes));
  nsresult rv = entry->GetEntryKey(entryKey);
  if (NS_FAILED(rv)) {
    return rv;
  }

  nsTArray<nsIDataStorage::DataType> typesToTry;
  if (aOriginAttributes.IsPrivateBrowsing()) {
    typesToTry.AppendElement(nsIDataStorage::DataType::Private);
  } else {
    typesToTry.AppendElement(nsIDataStorage::DataType::Persistent);
    typesToTry.AppendElement(nsIDataStorage::DataType::Temporary);
  }

  for (const auto& storageType : typesToTry) {
    nsAutoCString listEntry;
    rv = mClientAuthRememberList->Get(entryKey, storageType, listEntry);
    if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE) {
      return rv;
    }
    if (NS_SUCCEEDED(rv) && !listEntry.IsEmpty()) {
      if (!listEntry.Equals(nsClientAuthRemember::SentinelValue)) {
        aCertDBKey = listEntry;
      }
      *aRetVal = true;
      return NS_OK;
    }
  }

#ifdef XP_MACOSX
  rv = CheckForPreferredCertificate(aHostName, aCertDBKey);
  if (NS_FAILED(rv)) {
    return rv;
  }
  if (!aCertDBKey.IsEmpty()) {
    *aRetVal = true;
    return NS_OK;
  }
#endif

  return NS_OK;
}

NS_IMETHODIMP
nsClientAuthRememberService::HasRememberedDecisionScriptable(
    const nsACString& aHostName, JS::Handle<JS::Value> aOriginAttributes,
    nsACString& aCertDBKey, JSContext* aCx, bool* aRetVal) {
  OriginAttributes attrs;
  if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) {
    return NS_ERROR_INVALID_ARG;
  }
  return HasRememberedDecision(aHostName, attrs, aCertDBKey, aRetVal);
}

nsresult nsClientAuthRememberService::AddEntryToList(
    const nsACString& aHostName, const OriginAttributes& aOriginAttributes,
    const nsACString& aDBKey, Duration aDuration) {
  nsAutoCString entryKey;
  RefPtr<nsClientAuthRemember> entry(
      new nsClientAuthRemember(aHostName, aOriginAttributes));
  nsresult rv = entry->GetEntryKey(entryKey);
  if (NS_FAILED(rv)) {
    return rv;
  }

  nsIDataStorage::DataType storageType;
  if (aOriginAttributes.IsPrivateBrowsing()) {
    storageType = nsIDataStorage::DataType::Private;
  } else if (aDuration == nsIClientAuthRememberService::Duration::Permanent) {
    storageType = nsIDataStorage::DataType::Persistent;
  } else if (aDuration == nsIClientAuthRememberService::Duration::Session) {
    storageType = nsIDataStorage::DataType::Temporary;
  } else {
    return NS_ERROR_INVALID_ARG;
  }

  nsCString tmpDbKey(aDBKey);
  rv = mClientAuthRememberList->Put(entryKey, tmpDbKey, storageType);
  if (NS_FAILED(rv)) {
    return rv;
  }

  return NS_OK;
}

bool nsClientAuthRememberService::IsPrivateBrowsingKey(
    const nsCString& entryKey) {
  const int32_t separator = entryKey.Find(":");
  nsCString suffix;
  if (separator >= 0) {
    entryKey.Left(suffix, separator);
  } else {
    suffix = entryKey;
  }
  return OriginAttributes::IsPrivateBrowsing(suffix);
}
