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

#include "device/fido/pin.h"

#include <numeric>
#include <string>
#include <utility>

#include "base/compiler_specific.h"
#include "base/containers/span.h"
#include "base/containers/span_writer.h"
#include "base/containers/to_vector.h"
#include "base/i18n/char_iterator.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "components/cbor/reader.h"
#include "components/cbor/values.h"
#include "components/cbor/writer.h"
#include "crypto/keypair.h"
#include "device/fido/pin_internal.h"
#include "device/fido/public/fido_constants.h"
#include "third_party/boringssl/src/include/openssl/aes.h"
#include "third_party/boringssl/src/include/openssl/ec.h"
#include "third_party/boringssl/src/include/openssl/sha.h"

namespace device {
namespace pin {

namespace {

uint8_t PermissionsToByte(base::span<const pin::Permissions> permissions) {
  return std::accumulate(permissions.begin(), permissions.end(), 0,
                         [](uint8_t byte, pin::Permissions flag) {
                           return byte |= static_cast<uint8_t>(flag);
                         });
}

}  // namespace

// HasAtLeastFourCodepoints returns true if |pin| is UTF-8 encoded and contains
// four or more code points. This reflects the "4 Unicode characters"
// requirement in CTAP2.
static bool HasAtLeastFourCodepoints(const std::string& pin) {
  base::i18n::UTF8CharIterator it(pin);
  return it.Advance() && it.Advance() && it.Advance() && it.Advance();
}

PINEntryError ValidatePIN(const std::string& pin,
                          uint32_t min_pin_length,
                          std::optional<std::string> current_pin) {
  if (pin.size() < min_pin_length) {
    return PINEntryError::kTooShort;
  }
  if (pin.size() > kMaxBytes || pin.back() == 0 || !base::IsStringUTF8(pin)) {
    return PINEntryError::kInvalidCharacters;
  }
  if (!HasAtLeastFourCodepoints(pin)) {
    return PINEntryError::kTooShort;
  }
  if (pin == current_pin) {
    return pin::PINEntryError::kSameAsCurrentPIN;
  }
  return PINEntryError::kNoError;
}

PINEntryError ValidatePIN(const std::u16string& pin16,
                          uint32_t min_pin_length,
                          std::optional<std::string> current_pin) {
  std::string pin;
  if (!base::UTF16ToUTF8(pin16.c_str(), pin16.size(), &pin)) {
    return pin::PINEntryError::kInvalidCharacters;
  }
  return ValidatePIN(std::move(pin), min_pin_length, std::move(current_pin));
}

// EncodePINCommand returns a CTAP2 PIN command for the operation |subcommand|.
// Additional elements of the top-level CBOR map can be added with the optional
// |add_additional| callback.
static std::pair<CtapRequestCommand, std::optional<cbor::Value>>
EncodePINCommand(
    PINUVAuthProtocol protocol_version,
    Subcommand subcommand,
    std::function<void(cbor::Value::MapValue*)> add_additional = nullptr) {
  cbor::Value::MapValue map;
  map.emplace(static_cast<int>(RequestKey::kProtocol),
              static_cast<uint8_t>(protocol_version));
  map.emplace(static_cast<int>(RequestKey::kSubcommand),
              static_cast<int>(subcommand));

  if (add_additional) {
    add_additional(&map);
  }

  return std::make_pair(CtapRequestCommand::kAuthenticatorClientPin,
                        cbor::Value(std::move(map)));
}

RetriesResponse::RetriesResponse() = default;

// static
std::optional<RetriesResponse> RetriesResponse::ParsePinRetries(
    const std::optional<cbor::Value>& cbor) {
  return RetriesResponse::Parse(std::move(cbor),
                                static_cast<int>(ResponseKey::kRetries));
}

// static
std::optional<RetriesResponse> RetriesResponse::ParseUvRetries(
    const std::optional<cbor::Value>& cbor) {
  return RetriesResponse::Parse(std::move(cbor),
                                static_cast<int>(ResponseKey::kUvRetries));
}

// static
std::optional<RetriesResponse> RetriesResponse::Parse(
    const std::optional<cbor::Value>& cbor,
    const int retries_key) {
  if (!cbor || !cbor->is_map()) {
    return std::nullopt;
  }
  const auto& response_map = cbor->GetMap();

  auto it = response_map.find(cbor::Value(retries_key));
  if (it == response_map.end() || !it->second.is_unsigned()) {
    return std::nullopt;
  }

  const int64_t retries = it->second.GetUnsigned();
  if (retries > INT_MAX) {
    return std::nullopt;
  }

  RetriesResponse ret;
  ret.retries = static_cast<int>(retries);
  return ret;
}

KeyAgreementResponse::KeyAgreementResponse(crypto::keypair::PublicKey key)
    : key(key) {}

// static
std::optional<KeyAgreementResponse> KeyAgreementResponse::Parse(
    const std::optional<cbor::Value>& cbor) {
  if (!cbor || !cbor->is_map()) {
    return std::nullopt;
  }
  const auto& response_map = cbor->GetMap();

  // The ephemeral key is encoded as a COSE structure.
  auto it = response_map.find(
      cbor::Value(static_cast<int>(ResponseKey::kKeyAgreement)));
  if (it == response_map.end() || !it->second.is_map()) {
    return std::nullopt;
  }
  const auto& cose_key = it->second.GetMap();

  return ParseFromCOSE(cose_key);
}

// static
std::optional<KeyAgreementResponse> KeyAgreementResponse::ParseFromCOSE(
    const cbor::Value::MapValue& cose_key) {
  // The COSE key must be a P-256 point. See
  // https://tools.ietf.org/html/rfc8152#section-7.1
  for (const auto& pair : std::vector<std::pair<int, int>>({
           {1 /* key type */, 2 /* elliptic curve, uncompressed */},
           {3 /* algorithm */, -25 /* ECDH, ephemeral–static, HKDF-SHA-256 */},
           {-1 /* curve */, 1 /* P-256 */},
       })) {
    auto it = cose_key.find(cbor::Value(pair.first));
    if (it == cose_key.end() || !it->second.is_integer() ||
        it->second.GetInteger() != pair.second) {
      return std::nullopt;
    }
  }

  // See https://tools.ietf.org/html/rfc8152#section-13.1.1
  const auto& x_it = cose_key.find(cbor::Value(-2));
  const auto& y_it = cose_key.find(cbor::Value(-3));
  if (x_it == cose_key.end() || y_it == cose_key.end() ||
      !x_it->second.is_bytestring() || !y_it->second.is_bytestring()) {
    return std::nullopt;
  }

  const auto& x = x_it->second.GetBytestring();
  const auto& y = y_it->second.GetBytestring();
  std::array<uint8_t, kP256X962Length> x962;
  base::SpanWriter<uint8_t> writer(x962);
  // See https://datatracker.ietf.org/doc/html/rfc5480#section-2.2
  writer.WriteU8BigEndian(0x04);
  writer.Write(x);
  writer.Write(y);
  if (writer.num_written() != kP256X962Length) {
    return std::nullopt;
  }

  std::optional<crypto::keypair::PublicKey> key =
      crypto::keypair::PublicKey::FromEcP256Point(x962);

  if (!key) {
    // Point's not on the curve.
    return std::nullopt;
  }

  return KeyAgreementResponse(*key);
}

std::array<uint8_t, kP256X962Length> KeyAgreementResponse::X962() const {
  std::array<uint8_t, kP256X962Length> out;
  base::span(out).copy_from(key.ToUncompressedX962Point());
  return out;
}

SetRequest::SetRequest(PINUVAuthProtocol protocol,
                       const std::string& pin,
                       const KeyAgreementResponse& peer_key)
    : protocol_(protocol), peer_key_(peer_key) {
  DCHECK_EQ(ValidatePIN(pin), PINEntryError::kNoError);
  std::ranges::fill(pin_, 0);
  base::span(pin_).copy_prefix_from(base::as_byte_span(pin));
}

cbor::Value::MapValue EncodeCOSEPublicKey(
    base::span<const uint8_t, kP256X962Length> x962) {
  cbor::Value::MapValue cose_key;
  cose_key.emplace(1 /* key type */, 2 /* uncompressed elliptic curve */);
  cose_key.emplace(3 /* algorithm */,
                   -25 /* ECDH, ephemeral–static, HKDF-SHA-256 */);
  cose_key.emplace(-1 /* curve */, 1 /* P-256 */);
  const auto [x, y] = x962.subspan<1>().split_at<32>();
  cose_key.emplace(-2 /* x */, x);
  cose_key.emplace(-3 /* y */, y);

  return cose_key;
}

ChangeRequest::ChangeRequest(PINUVAuthProtocol protocol,
                             const std::string& old_pin,
                             const std::string& new_pin,
                             const KeyAgreementResponse& peer_key)
    : protocol_(protocol), peer_key_(peer_key) {
  uint8_t digest[SHA256_DIGEST_LENGTH];
  SHA256(reinterpret_cast<const uint8_t*>(old_pin.data()), old_pin.size(),
         digest);
  base::span(old_pin_hash_)
      .copy_from(base::span(digest).first(old_pin_hash_.size()));

  DCHECK_EQ(ValidatePIN(new_pin), PINEntryError::kNoError);
  new_pin_.fill(0);
  base::span(new_pin_).copy_prefix_from(base::as_byte_span(new_pin));
}

// static
std::optional<EmptyResponse> EmptyResponse::Parse(
    const std::optional<cbor::Value>& cbor) {
  // Yubikeys can return just the status byte, and no CBOR bytes, for the empty
  // response, which will end up here with |cbor| being |nullopt|. This seems
  // wrong, but is handled. (The response should, instead, encode an empty CBOR
  // map.)
  if (cbor && (!cbor->is_map() || !cbor->GetMap().empty())) {
    return std::nullopt;
  }

  EmptyResponse ret;
  return ret;
}

TokenResponse::TokenResponse(PINUVAuthProtocol protocol)
    : protocol_(protocol) {}
TokenResponse::~TokenResponse() = default;
TokenResponse::TokenResponse(const TokenResponse&) = default;
TokenResponse& TokenResponse::operator=(const TokenResponse&) = default;

std::optional<TokenResponse> TokenResponse::Parse(
    PINUVAuthProtocol protocol,
    base::span<const uint8_t> shared_key,
    const std::optional<cbor::Value>& cbor) {
  if (!cbor || !cbor->is_map()) {
    return std::nullopt;
  }
  const auto& response_map = cbor->GetMap();

  auto it =
      response_map.find(cbor::Value(static_cast<int>(ResponseKey::kPINToken)));
  if (it == response_map.end() || !it->second.is_bytestring()) {
    return std::nullopt;
  }
  const auto& encrypted_token = it->second.GetBytestring();
  if (encrypted_token.size() % AES_BLOCK_SIZE != 0) {
    return std::nullopt;
  }

  std::vector<uint8_t> token =
      ProtocolVersion(protocol).Decrypt(shared_key, encrypted_token);

  // The token must have the correct size for the given protocol.
  switch (protocol) {
    case PINUVAuthProtocol::kV1:
      // In CTAP2.1, V1 tokens are fixed at 16 or 32 bytes. But in CTAP2.0 they
      // may be any multiple of 16 bytes. We don't know the CTAP version, so
      // only enforce the latter.
      if (token.empty() || token.size() % AES_BLOCK_SIZE != 0) {
        return std::nullopt;
      }
      break;
    case PINUVAuthProtocol::kV2:
      if (token.size() != 32u) {
        return std::nullopt;
      }
      break;
  }

  TokenResponse ret(protocol);
  ret.token_ = std::move(token);
  return ret;
}

std::pair<PINUVAuthProtocol, std::vector<uint8_t>> TokenResponse::PinAuth(
    base::span<const uint8_t> client_data_hash) const {
  return {protocol_,
          ProtocolVersion(protocol_).Authenticate(token_, client_data_hash)};
}

// static
std::pair<CtapRequestCommand, std::optional<cbor::Value>>
AsCTAPRequestValuePair(const PinRetriesRequest& request) {
  return EncodePINCommand(request.protocol, Subcommand::kGetRetries);
}

// static
std::pair<CtapRequestCommand, std::optional<cbor::Value>>
AsCTAPRequestValuePair(const UvRetriesRequest& request) {
  return EncodePINCommand(request.protocol, Subcommand::kGetUvRetries);
}

// static
std::pair<CtapRequestCommand, std::optional<cbor::Value>>
AsCTAPRequestValuePair(const KeyAgreementRequest& request) {
  return EncodePINCommand(request.protocol, Subcommand::kGetKeyAgreement);
}

// static
std::pair<CtapRequestCommand, std::optional<cbor::Value>>
AsCTAPRequestValuePair(const SetRequest& request) {
  // See
  // https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.html#settingNewPin
  std::vector<uint8_t> shared_key;
  const Protocol& pin_protocol = ProtocolVersion(request.protocol_);
  auto cose_key = EncodeCOSEPublicKey(
      pin_protocol.Encapsulate(request.peer_key_, &shared_key));

  static_assert((sizeof(request.pin_) % AES_BLOCK_SIZE) == 0,
                "pin_ is not a multiple of the AES block size");
  std::vector<uint8_t> encrypted_pin =
      pin_protocol.Encrypt(shared_key, request.pin_);

  std::vector<uint8_t> pin_auth =
      pin_protocol.Authenticate(shared_key, encrypted_pin);

  return EncodePINCommand(
      request.protocol_, Subcommand::kSetPIN,
      [&cose_key, &encrypted_pin, &pin_auth](cbor::Value::MapValue* map) {
        map->emplace(static_cast<int>(RequestKey::kKeyAgreement),
                     std::move(cose_key));
        map->emplace(static_cast<int>(RequestKey::kNewPINEnc),
                     std::move(encrypted_pin));
        map->emplace(static_cast<int>(RequestKey::kPINAuth),
                     std::move(pin_auth));
      });
}

// static
std::pair<CtapRequestCommand, std::optional<cbor::Value>>
AsCTAPRequestValuePair(const ChangeRequest& request) {
  // See
  // https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.html#changingExistingPin
  std::vector<uint8_t> shared_key;
  const Protocol& pin_protocol = ProtocolVersion(request.protocol_);
  auto cose_key = EncodeCOSEPublicKey(
      pin_protocol.Encapsulate(request.peer_key_, &shared_key));

  static_assert((sizeof(request.new_pin_) % AES_BLOCK_SIZE) == 0,
                "new_pin_ is not a multiple of the AES block size");
  std::vector<uint8_t> encrypted_pin =
      pin_protocol.Encrypt(shared_key, request.new_pin_);

  static_assert((sizeof(request.old_pin_hash_) % AES_BLOCK_SIZE) == 0,
                "old_pin_hash_ is not a multiple of the AES block size");
  std::vector<uint8_t> old_pin_hash_enc =
      pin_protocol.Encrypt(shared_key, request.old_pin_hash_);

  std::vector<uint8_t> ciphertexts_concat(encrypted_pin.size() +
                                          old_pin_hash_enc.size());
  {
    auto [l, r] = base::span(ciphertexts_concat).split_at(encrypted_pin.size());
    l.copy_from(encrypted_pin);
    r.copy_from(old_pin_hash_enc);
  }

  std::vector<uint8_t> pin_auth =
      pin_protocol.Authenticate(shared_key, ciphertexts_concat);

  return EncodePINCommand(
      request.protocol_, Subcommand::kChangePIN,
      [&cose_key, &encrypted_pin, &old_pin_hash_enc,
       &pin_auth](cbor::Value::MapValue* map) {
        map->emplace(static_cast<int>(RequestKey::kKeyAgreement),
                     std::move(cose_key));
        map->emplace(static_cast<int>(RequestKey::kPINHashEnc),
                     std::move(old_pin_hash_enc));
        map->emplace(static_cast<int>(RequestKey::kNewPINEnc),
                     std::move(encrypted_pin));
        map->emplace(static_cast<int>(RequestKey::kPINAuth),
                     std::move(pin_auth));
      });
}

// static
std::pair<CtapRequestCommand, std::optional<cbor::Value>>
AsCTAPRequestValuePair(const ResetRequest&) {
  return std::make_pair(CtapRequestCommand::kAuthenticatorReset, std::nullopt);
}

TokenRequest::TokenRequest(PINUVAuthProtocol protocol,
                           const KeyAgreementResponse& peer_key)
    : protocol_(protocol),
      public_key_(
          ProtocolVersion(protocol_).Encapsulate(peer_key, &shared_key_)) {}

TokenRequest::~TokenRequest() = default;

TokenRequest::TokenRequest(TokenRequest&& other) = default;

const std::vector<uint8_t>& TokenRequest::shared_key() const {
  return shared_key_;
}

PinTokenRequest::PinTokenRequest(PINUVAuthProtocol protocol,
                                 const std::string& pin,
                                 const KeyAgreementResponse& peer_key)
    : TokenRequest(protocol, peer_key) {
  uint8_t digest[SHA256_DIGEST_LENGTH];
  SHA256(reinterpret_cast<const uint8_t*>(pin.data()), pin.size(), digest);
  base::span(pin_hash_).copy_from(base::span(digest).first(pin_hash_.size()));
}

PinTokenRequest::~PinTokenRequest() = default;

PinTokenRequest::PinTokenRequest(PinTokenRequest&& other) = default;

// static
std::pair<CtapRequestCommand, std::optional<cbor::Value>>
AsCTAPRequestValuePair(const PinTokenRequest& request) {
  static_assert((sizeof(request.pin_hash_) % AES_BLOCK_SIZE) == 0,
                "pin_hash_ is not a multiple of the AES block size");
  std::vector<uint8_t> encrypted_pin =
      ProtocolVersion(request.protocol_)
          .Encrypt(request.shared_key_, request.pin_hash_);

  return EncodePINCommand(
      request.protocol_, Subcommand::kGetPINToken,
      [&request, &encrypted_pin](cbor::Value::MapValue* map) {
        map->emplace(static_cast<int>(RequestKey::kKeyAgreement),
                     EncodeCOSEPublicKey(request.public_key_));
        map->emplace(static_cast<int>(RequestKey::kPINHashEnc),
                     std::move(encrypted_pin));
      });
}

PinTokenWithPermissionsRequest::PinTokenWithPermissionsRequest(
    PINUVAuthProtocol protocol,
    const std::string& pin,
    const KeyAgreementResponse& peer_key,
    base::span<const pin::Permissions> permissions,
    const std::optional<std::string> rp_id)
    : PinTokenRequest(protocol, pin, peer_key),
      permissions_(PermissionsToByte(permissions)),
      rp_id_(rp_id) {}

// static
std::pair<CtapRequestCommand, std::optional<cbor::Value>>
AsCTAPRequestValuePair(const PinTokenWithPermissionsRequest& request) {
  std::vector<uint8_t> encrypted_pin =
      ProtocolVersion(request.protocol_)
          .Encrypt(request.shared_key_, request.pin_hash_);

  return EncodePINCommand(
      request.protocol_, Subcommand::kGetPinUvAuthTokenUsingPinWithPermissions,
      [&request, &encrypted_pin](cbor::Value::MapValue* map) {
        map->emplace(static_cast<int>(RequestKey::kKeyAgreement),
                     EncodeCOSEPublicKey(request.public_key_));
        map->emplace(static_cast<int>(RequestKey::kPINHashEnc),
                     std::move(encrypted_pin));
        map->emplace(static_cast<int>(RequestKey::kPermissions),
                     std::move(request.permissions_));
        if (request.rp_id_) {
          map->emplace(static_cast<int>(RequestKey::kPermissionsRPID),
                       *request.rp_id_);
        }
      });
}

PinTokenWithPermissionsRequest::~PinTokenWithPermissionsRequest() = default;

PinTokenWithPermissionsRequest::PinTokenWithPermissionsRequest(
    PinTokenWithPermissionsRequest&& other) = default;

UvTokenRequest::UvTokenRequest(PINUVAuthProtocol protocol,
                               const KeyAgreementResponse& peer_key,
                               std::optional<std::string> rp_id,
                               base::span<const pin::Permissions> permissions)
    : TokenRequest(protocol, peer_key),
      rp_id_(rp_id),
      permissions_(PermissionsToByte(permissions)) {}

UvTokenRequest::~UvTokenRequest() = default;

UvTokenRequest::UvTokenRequest(UvTokenRequest&& other) = default;

// static
std::pair<CtapRequestCommand, std::optional<cbor::Value>>
AsCTAPRequestValuePair(const UvTokenRequest& request) {
  return EncodePINCommand(
      request.protocol_, Subcommand::kGetUvToken,
      [&request](cbor::Value::MapValue* map) {
        map->emplace(static_cast<int>(RequestKey::kKeyAgreement),
                     EncodeCOSEPublicKey(request.public_key_));
        map->emplace(static_cast<int>(RequestKey::kPermissions),
                     request.permissions_);
        if (request.rp_id_) {
          map->emplace(static_cast<int>(RequestKey::kPermissionsRPID),
                       *request.rp_id_);
        }
      });
}

static std::vector<uint8_t> ConcatSalts(
    base::span<const uint8_t, 32> salt1,
    const std::optional<std::array<uint8_t, 32>>& salt2) {
  if (!salt2) {
    return base::ToVector(salt1);
  }

  std::vector<uint8_t> salts(salt1.size() + salt2->size());
  auto [l, r] = base::span(salts).split_at(salt1.size());
  l.copy_from(salt1);
  r.copy_from(*salt2);
  return salts;
}

HMACSecretRequest::HMACSecretRequest(
    PINUVAuthProtocol protocol,
    const KeyAgreementResponse& peer_key,
    base::span<const uint8_t, 32> salt1,
    const std::optional<std::array<uint8_t, 32>>& salt2)
    : protocol_(protocol),
      have_two_salts_(salt2.has_value()),
      public_key_x962(
          ProtocolVersion(protocol_).Encapsulate(peer_key, &shared_key_)),
      encrypted_salts(
          ProtocolVersion(protocol_).Encrypt(shared_key_,
                                             ConcatSalts(salt1, salt2))),
      salts_auth(ProtocolVersion(protocol_).Authenticate(shared_key_,
                                                         encrypted_salts)) {}

HMACSecretRequest::~HMACSecretRequest() = default;

HMACSecretRequest::HMACSecretRequest(const HMACSecretRequest& other) = default;

std::optional<std::vector<uint8_t>> HMACSecretRequest::Decrypt(
    base::span<const uint8_t> ciphertext) {
  const std::optional<std::vector<uint8_t>> plaintext =
      pin::ProtocolVersion(protocol_).Decrypt(shared_key_, ciphertext);

  const unsigned num_salts = have_two_salts_ ? 2 : 1;
  if (plaintext && plaintext->size() != 32 * num_salts) {
    return std::nullopt;
  }

  return plaintext;
}

}  // namespace pin
}  // namespace device
