// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////

#include "tink/subtle/encrypt_then_authenticate.h"

#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "tink/aead.h"
#include "tink/internal/util.h"
#include "tink/mac.h"
#include "tink/subtle/ind_cpa_cipher.h"
#include "tink/util/errors.h"
#include "tink/util/status.h"
#include "tink/util/statusor.h"

namespace crypto {
namespace tink {
namespace subtle {

static std::string longToBigEndianStr(uint64_t value) {
  uint8_t bytes[8];
  for (int i = sizeof(bytes) - 1; i >= 0; i--) {
    bytes[i] = value & 0xff;
    value >>= 8;
  }
  return std::string(reinterpret_cast<const char*>(&bytes[0]), sizeof(bytes));
}

absl::StatusOr<std::unique_ptr<Aead>> EncryptThenAuthenticate::New(
    std::unique_ptr<IndCpaCipher> ind_cpa_cipher, std::unique_ptr<Mac> mac,
    uint8_t tag_size) {
  if (tag_size < kMinTagSizeInBytes) {
    return absl::Status(absl::StatusCode::kInvalidArgument,
                        "tag size too small");
  }
  std::unique_ptr<Aead> aead(new EncryptThenAuthenticate(
      std::move(ind_cpa_cipher), std::move(mac), tag_size));
  return std::move(aead);
}

absl::StatusOr<std::string> EncryptThenAuthenticate::Encrypt(
    absl::string_view plaintext, absl::string_view associated_data) const {
  // BoringSSL expects a non-null pointer for plaintext and associated_data,
  // regardless of whether the size is 0.
  plaintext = internal::EnsureStringNonNull(plaintext);
  associated_data = internal::EnsureStringNonNull(associated_data);

  uint64_t associated_data_size_in_bytes = associated_data.size();
  uint64_t associated_data_size_in_bits = associated_data_size_in_bytes * 8;
  if (associated_data_size_in_bits / 8 !=
      associated_data_size_in_bytes /* overflow occured! */) {
    return absl::Status(absl::StatusCode::kInvalidArgument,
                        "associated data too long");
  }

  auto ciphertext = ind_cpa_cipher_->Encrypt(plaintext);
  if (!ciphertext.ok()) {
    return ciphertext.status();
  }
  auto tag = mac_->ComputeMac(
      absl::StrCat(associated_data, *ciphertext,
                   longToBigEndianStr(associated_data_size_in_bits)));
  if (!tag.ok()) {
    return tag.status();
  }
  if (tag->size() != tag_size_) {
    return absl::Status(absl::StatusCode::kInternal, "invalid tag size");
  }
  return ciphertext->append(tag.value());
}

absl::StatusOr<std::string> EncryptThenAuthenticate::Decrypt(
    absl::string_view ciphertext, absl::string_view associated_data) const {
  // BoringSSL expects a non-null pointer for associated_data,
  // regardless of whether the size is 0.
  associated_data = internal::EnsureStringNonNull(associated_data);

  if (ciphertext.size() < tag_size_) {
    return absl::Status(absl::StatusCode::kInvalidArgument,
                        "ciphertext too short");
  }

  uint64_t associated_data_size_in_bytes = associated_data.size();
  uint64_t associated_data_size_in_bits = associated_data_size_in_bytes * 8;
  if (associated_data_size_in_bits / 8 !=
      associated_data_size_in_bytes /* overflow occured! */) {
    return absl::Status(absl::StatusCode::kInvalidArgument,
                        "additional data too long");
  }

  auto payload = ciphertext.substr(0, ciphertext.size() - tag_size_);
  auto tag = ciphertext.substr(ciphertext.size() - tag_size_, tag_size_);

  auto verified = mac_->VerifyMac(
      tag, absl::StrCat(associated_data, payload,
                        longToBigEndianStr(associated_data_size_in_bits)));
  if (!verified.ok()) {
    return verified;
  }

  auto pt = ind_cpa_cipher_->Decrypt(payload);
  if (!pt.ok()) {
    return pt.status();
  }

  return pt.value();
}

}  // namespace subtle
}  // namespace tink
}  // namespace crypto
