//
// Copyright (C) 2018 The Android Open Source Project
//
// 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 "update_engine/payload_consumer/payload_metadata.h"

#include <endian.h>

#include <brillo/data_encoding.h>

#include "update_engine/common/hash_calculator.h"
#include "update_engine/common/utils.h"
#include "update_engine/payload_consumer/payload_constants.h"
#include "update_engine/payload_consumer/payload_verifier.h"

namespace chromeos_update_engine {

const uint64_t PayloadMetadata::kDeltaVersionOffset = sizeof(kDeltaMagic);
const uint64_t PayloadMetadata::kDeltaVersionSize = 8;
const uint64_t PayloadMetadata::kDeltaManifestSizeOffset =
    kDeltaVersionOffset + kDeltaVersionSize;
const uint64_t PayloadMetadata::kDeltaManifestSizeSize = 8;
const uint64_t PayloadMetadata::kDeltaMetadataSignatureSizeSize = 4;

bool PayloadMetadata::GetMetadataSignatureSizeOffset(
    uint64_t* out_offset) const {
  if (GetMajorVersion() == kBrilloMajorPayloadVersion) {
    *out_offset = kDeltaManifestSizeOffset + kDeltaManifestSizeSize;
    return true;
  }
  return false;
}

bool PayloadMetadata::GetManifestOffset(uint64_t* out_offset) const {
  // Actual manifest begins right after the manifest size field or
  // metadata signature size field if major version >= 2.
  if (major_payload_version_ == kChromeOSMajorPayloadVersion) {
    *out_offset = kDeltaManifestSizeOffset + kDeltaManifestSizeSize;
    return true;
  }
  if (major_payload_version_ == kBrilloMajorPayloadVersion) {
    *out_offset = kDeltaManifestSizeOffset + kDeltaManifestSizeSize +
                  kDeltaMetadataSignatureSizeSize;
    return true;
  }
  LOG(ERROR) << "Unknown major payload version: " << major_payload_version_;
  return false;
}

MetadataParseResult PayloadMetadata::ParsePayloadHeader(
    const brillo::Blob& payload,
    uint64_t supported_major_version,
    ErrorCode* error) {
  uint64_t manifest_offset;
  // Ensure we have data to cover the major payload version.
  if (payload.size() < kDeltaManifestSizeOffset)
    return MetadataParseResult::kInsufficientData;

  // Validate the magic string.
  if (memcmp(payload.data(), kDeltaMagic, sizeof(kDeltaMagic)) != 0) {
    LOG(ERROR) << "Bad payload format -- invalid delta magic.";
    *error = ErrorCode::kDownloadInvalidMetadataMagicString;
    return MetadataParseResult::kError;
  }

  // Extract the payload version from the metadata.
  static_assert(sizeof(major_payload_version_) == kDeltaVersionSize,
                "Major payload version size mismatch");
  memcpy(&major_payload_version_,
         &payload[kDeltaVersionOffset],
         kDeltaVersionSize);
  // Switch big endian to host.
  major_payload_version_ = be64toh(major_payload_version_);

  if (major_payload_version_ != supported_major_version &&
      major_payload_version_ != kChromeOSMajorPayloadVersion) {
    LOG(ERROR) << "Bad payload format -- unsupported payload version: "
               << major_payload_version_;
    *error = ErrorCode::kUnsupportedMajorPayloadVersion;
    return MetadataParseResult::kError;
  }

  // Get the manifest offset now that we have payload version.
  if (!GetManifestOffset(&manifest_offset)) {
    *error = ErrorCode::kUnsupportedMajorPayloadVersion;
    return MetadataParseResult::kError;
  }
  // Check again with the manifest offset.
  if (payload.size() < manifest_offset)
    return MetadataParseResult::kInsufficientData;

  // Next, parse the manifest size.
  static_assert(sizeof(manifest_size_) == kDeltaManifestSizeSize,
                "manifest_size size mismatch");
  memcpy(&manifest_size_,
         &payload[kDeltaManifestSizeOffset],
         kDeltaManifestSizeSize);
  manifest_size_ = be64toh(manifest_size_);  // switch big endian to host

  if (GetMajorVersion() == kBrilloMajorPayloadVersion) {
    // Parse the metadata signature size.
    static_assert(
        sizeof(metadata_signature_size_) == kDeltaMetadataSignatureSizeSize,
        "metadata_signature_size size mismatch");
    uint64_t metadata_signature_size_offset;
    if (!GetMetadataSignatureSizeOffset(&metadata_signature_size_offset)) {
      *error = ErrorCode::kError;
      return MetadataParseResult::kError;
    }
    memcpy(&metadata_signature_size_,
           &payload[metadata_signature_size_offset],
           kDeltaMetadataSignatureSizeSize);
    metadata_signature_size_ = be32toh(metadata_signature_size_);
  }
  metadata_size_ = manifest_offset + manifest_size_;
  return MetadataParseResult::kSuccess;
}

bool PayloadMetadata::GetManifest(const brillo::Blob& payload,
                                  DeltaArchiveManifest* out_manifest) const {
  uint64_t manifest_offset;
  if (!GetManifestOffset(&manifest_offset))
    return false;
  CHECK_GE(payload.size(), manifest_offset + manifest_size_);
  return out_manifest->ParseFromArray(&payload[manifest_offset],
                                      manifest_size_);
}

ErrorCode PayloadMetadata::ValidateMetadataSignature(
    const brillo::Blob& payload,
    std::string metadata_signature,
    base::FilePath path_to_public_key) const {
  if (payload.size() < metadata_size_ + metadata_signature_size_)
    return ErrorCode::kDownloadMetadataSignatureError;

  brillo::Blob metadata_signature_blob, metadata_signature_protobuf_blob;
  if (!metadata_signature.empty()) {
    // Convert base64-encoded signature to raw bytes.
    if (!brillo::data_encoding::Base64Decode(metadata_signature,
                                             &metadata_signature_blob)) {
      LOG(ERROR) << "Unable to decode base64 metadata signature: "
                 << metadata_signature;
      return ErrorCode::kDownloadMetadataSignatureError;
    }
  } else if (major_payload_version_ == kBrilloMajorPayloadVersion) {
    metadata_signature_protobuf_blob.assign(
        payload.begin() + metadata_size_,
        payload.begin() + metadata_size_ + metadata_signature_size_);
  }

  if (metadata_signature_blob.empty() &&
      metadata_signature_protobuf_blob.empty()) {
    LOG(ERROR) << "Missing mandatory metadata signature in both Omaha "
               << "response and payload.";
    return ErrorCode::kDownloadMetadataSignatureMissingError;
  }

  LOG(INFO) << "Verifying metadata hash signature using public key: "
            << path_to_public_key.value();

  brillo::Blob calculated_metadata_hash;
  if (!HashCalculator::RawHashOfBytes(
          payload.data(), metadata_size_, &calculated_metadata_hash)) {
    LOG(ERROR) << "Unable to compute actual hash of manifest";
    return ErrorCode::kDownloadMetadataSignatureVerificationError;
  }

  PayloadVerifier::PadRSA2048SHA256Hash(&calculated_metadata_hash);
  if (calculated_metadata_hash.empty()) {
    LOG(ERROR) << "Computed actual hash of metadata is empty.";
    return ErrorCode::kDownloadMetadataSignatureVerificationError;
  }

  if (!metadata_signature_blob.empty()) {
    brillo::Blob expected_metadata_hash;
    if (!PayloadVerifier::GetRawHashFromSignature(metadata_signature_blob,
                                                  path_to_public_key.value(),
                                                  &expected_metadata_hash)) {
      LOG(ERROR) << "Unable to compute expected hash from metadata signature";
      return ErrorCode::kDownloadMetadataSignatureError;
    }
    if (calculated_metadata_hash != expected_metadata_hash) {
      LOG(ERROR) << "Manifest hash verification failed. Expected hash = ";
      utils::HexDumpVector(expected_metadata_hash);
      LOG(ERROR) << "Calculated hash = ";
      utils::HexDumpVector(calculated_metadata_hash);
      return ErrorCode::kDownloadMetadataSignatureMismatch;
    }
  } else {
    if (!PayloadVerifier::VerifySignature(metadata_signature_protobuf_blob,
                                          path_to_public_key.value(),
                                          calculated_metadata_hash)) {
      LOG(ERROR) << "Manifest hash verification failed.";
      return ErrorCode::kDownloadMetadataSignatureMismatch;
    }
  }

  // The autoupdate_CatchBadSignatures test checks for this string in
  // log-files. Keep in sync.
  LOG(INFO) << "Metadata hash signature matches value in Omaha response.";
  return ErrorCode::kSuccess;
}

}  // namespace chromeos_update_engine