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

#include "WebAuthnEnumStrings.h"
#include "WebAuthnTransportIdentifiers.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_security.h"
#include "nsIObserverService.h"
#include "nsTextFormatter.h"
#include "nsThreadUtils.h"

namespace mozilla::dom {

already_AddRefed<nsIWebAuthnService> NewWebAuthnService() {
  nsCOMPtr<nsIWebAuthnService> webauthnService(new WebAuthnService());
  return webauthnService.forget();
}

NS_IMPL_ISUPPORTS(WebAuthnService, nsIWebAuthnService)

void WebAuthnService::ShowAttestationConsentPrompt(
    const nsString& aOrigin, uint64_t aTransactionId,
    uint64_t aBrowsingContextId) {
  RefPtr<WebAuthnService> self = this;
#ifdef MOZ_WIDGET_ANDROID
  // We don't have a way to prompt the user for consent on Android, so just
  // assume consent not granted.
  nsCOMPtr<nsIRunnable> runnable(
      NS_NewRunnableFunction(__func__, [self, aTransactionId]() {
        self->SetHasAttestationConsent(
            aTransactionId,
            StaticPrefs::security_webauthn_always_allow_direct_attestation());
      }));
#else
  nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction(
      __func__, [self, aOrigin, aTransactionId, aBrowsingContextId]() {
        if (StaticPrefs::security_webauthn_always_allow_direct_attestation()) {
          self->SetHasAttestationConsent(aTransactionId, true);
          return;
        }
        nsCOMPtr<nsIObserverService> os = services::GetObserverService();
        if (!os) {
          return;
        }
        const nsLiteralString jsonFmt =
            u"{\"prompt\": {\"type\":\"attestation-consent\"},"_ns
            u"\"origin\": \"%S\","_ns
            u"\"tid\": %llu, \"browsingContextId\": %llu}"_ns;
        nsString json;
        nsTextFormatter::ssprintf(json, jsonFmt.get(), aOrigin.get(),
                                  aTransactionId, aBrowsingContextId);
        MOZ_ALWAYS_SUCCEEDS(
            os->NotifyObservers(nullptr, "webauthn-prompt", json.get()));
      }));
#endif
  NS_DispatchToMainThread(runnable.forget());
}

NS_IMETHODIMP
WebAuthnService::MakeCredential(uint64_t aTransactionId,
                                uint64_t aBrowsingContextId,
                                nsIWebAuthnRegisterArgs* aArgs,
                                nsIWebAuthnRegisterPromise* aPromise) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aArgs);
  MOZ_ASSERT(aPromise);

  ResetActiveTransaction();
  mActiveTransaction =
      Some(TransactionState{.service = DefaultService(),
                            .transactionId = aTransactionId,
                            .parentRegisterPromise = Some(aPromise)});

  // We may need to show an attestation consent prompt before we return a
  // credential to WebAuthnTransactionParent, so we insert a new promise that
  // chains to `aPromise` here.

  nsString attestation;
  (void)aArgs->GetAttestationConveyancePreference(attestation);
  bool attestationRequested = !attestation.EqualsLiteral(
      MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE);

  nsString origin;
  (void)aArgs->GetOrigin(origin);

  RefPtr<WebAuthnRegisterPromiseHolder> promiseHolder =
      new WebAuthnRegisterPromiseHolder(GetCurrentSerialEventTarget());

  RefPtr<WebAuthnService> self = this;
  RefPtr<WebAuthnRegisterPromise> promise = promiseHolder->Ensure();
  promise
      ->Then(
          GetCurrentSerialEventTarget(), __func__,
          [self, origin, aTransactionId, aBrowsingContextId,
           attestationRequested](
              const WebAuthnRegisterPromise::ResolveOrRejectValue& aValue) {
            MOZ_ASSERT(NS_IsMainThread());
            if (self->mActiveTransaction.isNothing()) {
              return;
            }
            MOZ_ASSERT(
                self->mActiveTransaction.ref().parentRegisterPromise.isSome());
            MOZ_ASSERT(
                self->mActiveTransaction.ref().registerResult.isNothing());
            MOZ_ASSERT(
                self->mActiveTransaction.ref().childRegisterRequest.Exists());

            self->mActiveTransaction.ref().childRegisterRequest.Complete();

            if (aValue.IsReject()) {
              self->mActiveTransaction.ref()
                  .parentRegisterPromise.ref()
                  ->Reject(aValue.RejectValue());
              self->mActiveTransaction.reset();
              return;
            }

            nsIWebAuthnRegisterResult* result = aValue.ResolveValue();
            // We can return whatever result we have if the authenticator
            // handled attestation consent for us.
            bool attestationConsentPromptShown = false;
            (void)result->GetAttestationConsentPromptShown(
                &attestationConsentPromptShown);
            if (attestationConsentPromptShown) {
              self->mActiveTransaction.ref()
                  .parentRegisterPromise.ref()
                  ->Resolve(result);
              self->mActiveTransaction.reset();
              return;
            }
            // If the RP requested attestation and the response contains
            // identifying information, then we need to show a consent
            // prompt.
            bool resultIsIdentifying = true;
            (void)result->HasIdentifyingAttestation(&resultIsIdentifying);
            if (attestationRequested && resultIsIdentifying) {
              self->mActiveTransaction.ref().registerResult = Some(result);
              self->ShowAttestationConsentPrompt(origin, aTransactionId,
                                                 aBrowsingContextId);
              return;
            }
            // In all other cases we strip out identifying information.
            result->Anonymize();
            self->mActiveTransaction.ref().parentRegisterPromise.ref()->Resolve(
                result);
            self->mActiveTransaction.reset();
          })
      ->Track(mActiveTransaction.ref().childRegisterRequest);

  nsresult rv = ActiveService()->MakeCredential(
      aTransactionId, aBrowsingContextId, aArgs, promiseHolder);
  if (NS_FAILED(rv)) {
    promiseHolder->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
  }
  return NS_OK;
}

NS_IMETHODIMP
WebAuthnService::GetAssertion(uint64_t aTransactionId,
                              uint64_t aBrowsingContextId,
                              nsIWebAuthnSignArgs* aArgs,
                              nsIWebAuthnSignPromise* aPromise) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aArgs);
  MOZ_ASSERT(aPromise);

  bool conditionallyMediated;
  (void)aArgs->GetConditionallyMediated(&conditionallyMediated);

  if (conditionallyMediated) {
    // Conditional gets coexist with transactions from other browsing
    // contexts. We hold them here until they are promoted to a modal flow via
    // SelectAutoFillEntry or ResumeConditionalGet, or cancelled.
    mConditionalGets.RemoveElementsBy(
        [aBrowsingContextId](ConditionalGet& aEntry) {
          if (aEntry.browsingContextId != aBrowsingContextId) {
            return false;
          }
          aEntry.signPromise->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
          return true;
        });
    mConditionalGets.AppendElement(ConditionalGet{
        .transactionId = aTransactionId,
        .browsingContextId = aBrowsingContextId,
        .signArgs = aArgs,
        .signPromise = aPromise,
    });

    // Defer the observer notification to the next event-loop turn so it fires
    // after this call has fully returned to the caller.
    nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction(__func__, []() {
      nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
      if (os) {
        os->NotifyObservers(nullptr, "webauthn:conditional-get-pending",
                            nullptr);
      }
    }));
    NS_DispatchToMainThread(runnable.forget());
    return NS_OK;
  }

  ResetActiveTransaction();
  mActiveTransaction = Some(TransactionState{.service = DefaultService(),
                                             .transactionId = aTransactionId});

#if defined(XP_MACOSX)
  if (__builtin_available(macos 14.5, *)) {
    // This branch is intentionally empty; using `!__builtin_available(...)`
    // results in a compiler warning.
  } else {
    // On macOS < 14.5 the security key API doesn't handle the AppID
    // extension. So we'll use authenticator-rs if it's likely that the
    // request requires AppID. We consider it likely if 1) the AppID
    // extension is present, 2) the allow list is non-empty, and 3) none of
    // the allowed credentials use the "internal" or "hybrid" transport.
    nsString appId;
    nsresult rv = aArgs->GetAppId(appId);
    if (rv == NS_OK) {  // AppID is set
      uint8_t transportSet = 0;
      nsTArray<uint8_t> allowListTransports;
      (void)aArgs->GetAllowListTransports(allowListTransports);
      for (const uint8_t& transport : allowListTransports) {
        transportSet |= transport;
      }
      uint8_t passkeyTransportMask =
          MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_INTERNAL |
          MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_HYBRID;
      if (allowListTransports.Length() > 0 &&
          (transportSet & passkeyTransportMask) == 0) {
        mActiveTransaction.ref().service = AuthrsService();
      }
    }
  }
#endif

  nsresult rv = ActiveService()->GetAssertion(
      aTransactionId, aBrowsingContextId, aArgs, aPromise);
  if (NS_FAILED(rv)) {
    mActiveTransaction.reset();
    return rv;
  }
  return NS_OK;
}

NS_IMETHODIMP
WebAuthnService::GetIsUVPAA(bool* aAvailable) {
  return DefaultService()->GetIsUVPAA(aAvailable);
}

NS_IMETHODIMP
WebAuthnService::HasPendingConditionalGet(uint64_t aBrowsingContextId,
                                          const nsAString& aOrigin,
                                          uint64_t* aRv) {
  MOZ_ASSERT(NS_IsMainThread());
  for (const auto& entry : mConditionalGets) {
    if (entry.browsingContextId != aBrowsingContextId) {
      continue;
    }
    nsString entryOrigin;
    (void)entry.signArgs->GetOrigin(entryOrigin);
    if (entryOrigin == aOrigin) {
      *aRv = entry.transactionId;
      return NS_OK;
    }
  }
  *aRv = 0;
  return NS_OK;
}

NS_IMETHODIMP
WebAuthnService::GetAutoFillEntries(
    uint64_t aTransactionId, nsIWebAuthnAutoFillEntriesCallback* aCallback) {
  MOZ_ASSERT(NS_IsMainThread());
  nsCOMPtr<nsIWebAuthnSignArgs> signArgs;
  for (const auto& entry : mConditionalGets) {
    if (entry.transactionId == aTransactionId) {
      signArgs = entry.signArgs;
      break;
    }
  }
  if (!signArgs) {
    aCallback->Reject(NS_ERROR_NOT_AVAILABLE);
    return NS_OK;
  }
  nsString rpId;
  nsTArray<nsTArray<uint8_t>> allowList;
  (void)signArgs->GetRpId(rpId);
  (void)signArgs->GetAllowList(allowList);
  return DefaultService()->GetAutoFillEntriesForRpId(rpId, allowList,
                                                     aCallback);
}

NS_IMETHODIMP
WebAuthnService::GetAutoFillEntriesForRpId(
    const nsAString& aRpId, const nsTArray<nsTArray<uint8_t>>& aAllowList,
    nsIWebAuthnAutoFillEntriesCallback* aCallback) {
  MOZ_ASSERT(NS_IsMainThread());
  return DefaultService()->GetAutoFillEntriesForRpId(aRpId, aAllowList,
                                                     aCallback);
}

Maybe<WebAuthnService::ConditionalGet> WebAuthnService::TakeConditionalByTid(
    uint64_t aTransactionId) {
  MOZ_ASSERT(NS_IsMainThread());
  Maybe<ConditionalGet> found;
  mConditionalGets.RemoveElementsBy([&](ConditionalGet& aEntry) {
    if (aEntry.transactionId != aTransactionId) {
      return false;
    }
    found = Some(std::move(aEntry));
    return true;
  });
  return found;
}

nsresult WebAuthnService::DispatchConditionalGetAssertion(
    const ConditionalGet& aPending, nsIWebAuthnSignArgs* aArgs) {
  MOZ_ASSERT(NS_IsMainThread());
  ResetActiveTransaction();
  mActiveTransaction = Some(TransactionState{
      .service = DefaultService(), .transactionId = aPending.transactionId});
  nsresult rv = ActiveService()->GetAssertion(aPending.transactionId,
                                              aPending.browsingContextId, aArgs,
                                              aPending.signPromise);
  if (NS_FAILED(rv)) {
    aPending.signPromise->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
    mActiveTransaction.reset();
    return rv;
  }
  return NS_OK;
}

NS_IMETHODIMP
WebAuthnService::SelectAutoFillEntry(uint64_t aTransactionId,
                                     const nsTArray<uint8_t>& aCredentialId) {
  MOZ_ASSERT(NS_IsMainThread());
  Maybe<ConditionalGet> found = TakeConditionalByTid(aTransactionId);
  if (found.isNothing()) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  // If the original allow list is non-empty, the selected credential must be
  // in it.
  nsTArray<nsTArray<uint8_t>> allowList;
  (void)found.ref().signArgs->GetAllowList(allowList);
  if (!allowList.IsEmpty() && !allowList.Contains(aCredentialId)) {
    found.ref().signPromise->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
    return NS_ERROR_FAILURE;
  }

  nsCOMPtr<nsIWebAuthnSignArgs> args;
  nsresult rv = found.ref().signArgs->CloneWithSelectedCredential(
      aCredentialId, getter_AddRefs(args));
  if (NS_FAILED(rv)) {
    found.ref().signPromise->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
    return rv;
  }
  return DispatchConditionalGetAssertion(found.ref(), args);
}

NS_IMETHODIMP
WebAuthnService::ResumeConditionalGet(uint64_t aTransactionId) {
  MOZ_ASSERT(NS_IsMainThread());
  Maybe<ConditionalGet> found = TakeConditionalByTid(aTransactionId);
  if (found.isNothing()) {
    return NS_ERROR_NOT_AVAILABLE;
  }
  return DispatchConditionalGetAssertion(found.ref(), found.ref().signArgs);
}

void WebAuthnService::RejectActiveRegisterPromise() {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mActiveTransaction.isSome());
  mActiveTransaction.ref().childRegisterRequest.DisconnectIfExists();
  if (mActiveTransaction.ref().parentRegisterPromise.isSome()) {
    mActiveTransaction.ref().parentRegisterPromise.ref()->Reject(
        NS_ERROR_DOM_NOT_ALLOWED_ERR);
  }
}

void WebAuthnService::ResetActiveTransaction() {
  MOZ_ASSERT(NS_IsMainThread());
  if (mActiveTransaction.isSome()) {
    RejectActiveRegisterPromise();
    ActiveService()->Reset();
    mActiveTransaction.reset();
  }
}

NS_IMETHODIMP
WebAuthnService::Reset() {
  MOZ_ASSERT(NS_IsMainThread());
  ResetActiveTransaction();
  return NS_OK;
}

NS_IMETHODIMP
WebAuthnService::Cancel(uint64_t aTransactionId) {
  MOZ_ASSERT(NS_IsMainThread());
  if (Maybe<ConditionalGet> found = TakeConditionalByTid(aTransactionId);
      found.isSome()) {
    found.ref().signPromise->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
    return NS_OK;
  }

  if (mActiveTransaction.isSome() &&
      mActiveTransaction.ref().transactionId == aTransactionId) {
    RejectActiveRegisterPromise();
    ActiveService()->Cancel(aTransactionId);
    mActiveTransaction.reset();
    return NS_OK;
  }

  // about:webauthn drives an interactive management session through Listen()
  // and RunCommand(), which always run on the authrs service and are not
  // tracked as an mActiveTransaction. Its Cancel() must reach authrs too, or
  // the session's status receiver (a blocking background task) is never torn
  // down and blocks shutdown.
  AuthrsService()->Cancel(aTransactionId);
  return NS_OK;
}

NS_IMETHODIMP
WebAuthnService::SetHasAttestationConsent(uint64_t aTransactionId,
                                          bool aHasConsent) {
  MOZ_ASSERT(NS_IsMainThread());
  if (mActiveTransaction.isNothing() ||
      mActiveTransaction.ref().transactionId != aTransactionId) {
    // This could happen if the transaction was reset just when the prompt was
    // receiving user input.
    return NS_OK;
  }

  MOZ_ASSERT(mActiveTransaction.ref().parentRegisterPromise.isSome());
  MOZ_ASSERT(mActiveTransaction.ref().registerResult.isSome());
  MOZ_ASSERT(!mActiveTransaction.ref().childRegisterRequest.Exists());

  if (!aHasConsent) {
    mActiveTransaction.ref().registerResult.ref()->Anonymize();
  }
  mActiveTransaction.ref().parentRegisterPromise.ref()->Resolve(
      mActiveTransaction.ref().registerResult.ref());

  mActiveTransaction.reset();
  return NS_OK;
}

NS_IMETHODIMP
WebAuthnService::PinCallback(uint64_t aTransactionId, const nsACString& aPin) {
  MOZ_ASSERT(NS_IsMainThread());
  if (mActiveTransaction.isNothing() ||
      mActiveTransaction.ref().transactionId != aTransactionId) {
    return NS_OK;
  }
  return ActiveService()->PinCallback(aTransactionId, aPin);
}

NS_IMETHODIMP
WebAuthnService::SelectionCallback(uint64_t aTransactionId, uint64_t aIndex) {
  MOZ_ASSERT(NS_IsMainThread());
  if (mActiveTransaction.isNothing() ||
      mActiveTransaction.ref().transactionId != aTransactionId) {
    return NS_OK;
  }
  return ActiveService()->SelectionCallback(aTransactionId, aIndex);
}

NS_IMETHODIMP
WebAuthnService::AddVirtualAuthenticator(
    const nsACString& aProtocol, const nsACString& aTransport,
    bool aHasResidentKey, bool aHasUserVerification, bool aIsUserConsenting,
    bool aIsUserVerified, nsACString& aRetval) {
  MOZ_ASSERT(NS_IsMainThread());
  return AuthrsService()->AddVirtualAuthenticator(
      aProtocol, aTransport, aHasResidentKey, aHasUserVerification,
      aIsUserConsenting, aIsUserVerified, aRetval);
}

NS_IMETHODIMP
WebAuthnService::RemoveVirtualAuthenticator(
    const nsACString& aAuthenticatorId) {
  MOZ_ASSERT(NS_IsMainThread());
  return AuthrsService()->RemoveVirtualAuthenticator(aAuthenticatorId);
}

NS_IMETHODIMP
WebAuthnService::AddCredential(const nsACString& aAuthenticatorId,
                               const nsACString& aCredentialId,
                               bool aIsResidentCredential,
                               const nsACString& aRpId,
                               const nsACString& aPrivateKey,
                               const nsACString& aUserHandle,
                               uint32_t aSignCount) {
  MOZ_ASSERT(NS_IsMainThread());
  return AuthrsService()->AddCredential(aAuthenticatorId, aCredentialId,
                                        aIsResidentCredential, aRpId,
                                        aPrivateKey, aUserHandle, aSignCount);
}

NS_IMETHODIMP
WebAuthnService::GetCredentials(
    const nsACString& aAuthenticatorId,
    nsTArray<RefPtr<nsICredentialParameters>>& aRetval) {
  MOZ_ASSERT(NS_IsMainThread());
  return AuthrsService()->GetCredentials(aAuthenticatorId, aRetval);
}

NS_IMETHODIMP
WebAuthnService::RemoveCredential(const nsACString& aAuthenticatorId,
                                  const nsACString& aCredentialId) {
  MOZ_ASSERT(NS_IsMainThread());
  return AuthrsService()->RemoveCredential(aAuthenticatorId, aCredentialId);
}

NS_IMETHODIMP
WebAuthnService::RemoveAllCredentials(const nsACString& aAuthenticatorId) {
  MOZ_ASSERT(NS_IsMainThread());
  return AuthrsService()->RemoveAllCredentials(aAuthenticatorId);
}

NS_IMETHODIMP
WebAuthnService::SetUserVerified(const nsACString& aAuthenticatorId,
                                 bool aIsUserVerified) {
  MOZ_ASSERT(NS_IsMainThread());
  return AuthrsService()->SetUserVerified(aAuthenticatorId, aIsUserVerified);
}

NS_IMETHODIMP
WebAuthnService::Listen() {
  MOZ_ASSERT(NS_IsMainThread());
  return AuthrsService()->Listen();
}

NS_IMETHODIMP
WebAuthnService::RunCommand(const nsACString& aCmd) {
  MOZ_ASSERT(NS_IsMainThread());
  return AuthrsService()->RunCommand(aCmd);
}

}  // namespace mozilla::dom
