/* 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 "mozilla/dom/AuthenticatorAttestationResponse.h"

#include "AuthrsBridge_ffi.h"
#include "mozilla/Base64.h"
#include "mozilla/dom/WebAuthenticationBinding.h"

namespace mozilla::dom {

NS_IMPL_CYCLE_COLLECTION_CLASS(AuthenticatorAttestationResponse)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(
    AuthenticatorAttestationResponse, AuthenticatorResponse)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(AuthenticatorAttestationResponse,
                                               AuthenticatorResponse)
  NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_TRACE_END

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(
    AuthenticatorAttestationResponse, AuthenticatorResponse)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

NS_IMPL_ADDREF_INHERITED(AuthenticatorAttestationResponse,
                         AuthenticatorResponse)
NS_IMPL_RELEASE_INHERITED(AuthenticatorAttestationResponse,
                          AuthenticatorResponse)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AuthenticatorAttestationResponse)
NS_INTERFACE_MAP_END_INHERITING(AuthenticatorResponse)

AuthenticatorAttestationResponse::AuthenticatorAttestationResponse(
    nsPIDOMWindowInner* aParent)
    : AuthenticatorResponse(aParent) {}

JSObject* AuthenticatorAttestationResponse::WrapObject(
    JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
  return AuthenticatorAttestationResponse_Binding::Wrap(aCx, this, aGivenProto);
}

void AuthenticatorAttestationResponse::GetAttestationObject(
    JSContext* aCx, JS::MutableHandle<JSObject*> aValue, ErrorResult& aRv) {
  aValue.set(ArrayBuffer::Create(aCx, mAttestationObject, aRv));
}

void AuthenticatorAttestationResponse::SetAttestationObject(
    const nsTArray<uint8_t>& aBuffer) {
  mAttestationObject.Assign(aBuffer);
}

void AuthenticatorAttestationResponse::GetTransports(
    nsTArray<nsString>& aTransports) {
  aTransports.Assign(mTransports);
}

void AuthenticatorAttestationResponse::SetTransports(
    const nsTArray<nsString>& aTransports) {
  mTransports.Assign(aTransports);
}

nsresult AuthenticatorAttestationResponse::GetAuthenticatorDataBytes(
    nsTArray<uint8_t>& aAuthenticatorData) {
  if (!mAttestationObjectParsed) {
    nsresult rv = authrs_webauthn_att_obj_constructor(
        mAttestationObject, /* anonymize */ false,
        getter_AddRefs(mAttestationObjectParsed));
    if (NS_FAILED(rv)) {
      return rv;
    }
  }
  nsresult rv =
      mAttestationObjectParsed->GetAuthenticatorData(aAuthenticatorData);
  if (NS_FAILED(rv)) {
    return rv;
  }
  return NS_OK;
}

void AuthenticatorAttestationResponse::GetAuthenticatorData(
    JSContext* aCx, JS::MutableHandle<JSObject*> aValue, ErrorResult& aRv) {
  nsTArray<uint8_t> authenticatorData;
  nsresult rv = GetAuthenticatorDataBytes(authenticatorData);
  if (NS_FAILED(rv)) {
    aRv.Throw(rv);
    return;
  }

  JS::Heap<JSObject*> buffer(ArrayBuffer::Create(aCx, authenticatorData, aRv));
  if (aRv.Failed()) {
    return;
  }
  aValue.set(buffer);
}

nsresult AuthenticatorAttestationResponse::GetPublicKeyBytes(
    nsTArray<uint8_t>& aPublicKeyBytes) {
  if (!mAttestationObjectParsed) {
    nsresult rv = authrs_webauthn_att_obj_constructor(
        mAttestationObject, /* anonymize */ false,
        getter_AddRefs(mAttestationObjectParsed));
    if (NS_FAILED(rv)) {
      return rv;
    }
  }
  nsresult rv = mAttestationObjectParsed->GetPublicKey(aPublicKeyBytes);
  if (NS_FAILED(rv)) {
    return rv;
  }
  return NS_OK;
}

void AuthenticatorAttestationResponse::GetPublicKey(
    JSContext* aCx, JS::MutableHandle<JSObject*> aValue, ErrorResult& aRv) {
  nsTArray<uint8_t> publicKey;
  nsresult rv = GetPublicKeyBytes(publicKey);
  if (NS_FAILED(rv)) {
    if (rv == NS_ERROR_NOT_AVAILABLE) {
      aValue.set(nullptr);
    } else {
      aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
    }
    return;
  }

  JS::Heap<JSObject*> buffer(ArrayBuffer::Create(aCx, publicKey, aRv));
  if (aRv.Failed()) {
    return;
  }
  aValue.set(buffer);
}

COSEAlgorithmIdentifier AuthenticatorAttestationResponse::GetPublicKeyAlgorithm(
    ErrorResult& aRv) {
  if (!mAttestationObjectParsed) {
    nsresult rv = authrs_webauthn_att_obj_constructor(
        mAttestationObject, false, getter_AddRefs(mAttestationObjectParsed));
    if (NS_FAILED(rv)) {
      aRv.Throw(rv);
      return 0;
    }
  }

  COSEAlgorithmIdentifier alg;
  mAttestationObjectParsed->GetPublicKeyAlgorithm(&alg);
  return alg;
}

void AuthenticatorAttestationResponse::ToJSON(
    AuthenticatorAttestationResponseJSON& aJSON, ErrorResult& aError) {
  nsAutoCString clientDataJSONBase64;
  nsresult rv = Base64URLEncode(
      mClientDataJSON.Length(),
      reinterpret_cast<const uint8_t*>(mClientDataJSON.get()),
      mozilla::Base64URLEncodePaddingPolicy::Omit, clientDataJSONBase64);
  // This will only fail if the length is so long that it overflows 32-bits
  // when calculating the encoded size.
  if (NS_FAILED(rv)) {
    aError.ThrowDataError("clientDataJSON too long");
    return;
  }
  aJSON.mClientDataJSON.Assign(NS_ConvertUTF8toUTF16(clientDataJSONBase64));

  nsTArray<uint8_t> authenticatorData;
  rv = GetAuthenticatorDataBytes(authenticatorData);
  if (NS_FAILED(rv)) {
    aError.ThrowUnknownError("could not get authenticatorData");
    return;
  }
  nsAutoCString authenticatorDataBase64;
  rv = Base64URLEncode(authenticatorData.Length(), authenticatorData.Elements(),
                       mozilla::Base64URLEncodePaddingPolicy::Omit,
                       authenticatorDataBase64);
  if (NS_FAILED(rv)) {
    aError.ThrowDataError("authenticatorData too long");
    return;
  }
  aJSON.mAuthenticatorData.Assign(
      NS_ConvertUTF8toUTF16(authenticatorDataBase64));

  if (!aJSON.mTransports.Assign(mTransports, mozilla::fallible)) {
    aError.Throw(NS_ERROR_OUT_OF_MEMORY);
    return;
  }

  nsTArray<uint8_t> publicKeyBytes;
  rv = GetPublicKeyBytes(publicKeyBytes);
  if (NS_SUCCEEDED(rv)) {
    nsAutoCString publicKeyBytesBase64;
    rv = Base64URLEncode(publicKeyBytes.Length(), publicKeyBytes.Elements(),
                         mozilla::Base64URLEncodePaddingPolicy::Omit,
                         publicKeyBytesBase64);
    if (NS_FAILED(rv)) {
      aError.ThrowDataError("publicKey too long");
      return;
    }
    aJSON.mPublicKey.Construct(NS_ConvertUTF8toUTF16(publicKeyBytesBase64));
  } else if (rv != NS_ERROR_NOT_AVAILABLE) {
    aError.ThrowUnknownError("could not get publicKey");
    return;
  }

  COSEAlgorithmIdentifier publicKeyAlgorithm = GetPublicKeyAlgorithm(aError);
  if (aError.Failed()) {
    aError.ThrowUnknownError("could not get publicKeyAlgorithm");
    return;
  }
  aJSON.mPublicKeyAlgorithm = publicKeyAlgorithm;

  nsAutoCString attestationObjectBase64;
  rv = Base64URLEncode(
      mAttestationObject.Length(), mAttestationObject.Elements(),
      mozilla::Base64URLEncodePaddingPolicy::Omit, attestationObjectBase64);
  if (NS_FAILED(rv)) {
    aError.ThrowDataError("attestationObject too long");
    return;
  }
  aJSON.mAttestationObject.Assign(
      NS_ConvertUTF8toUTF16(attestationObjectBase64));
}

}  // namespace mozilla::dom
