packages feed

wireform-parquet-0.1.0.0: scripts/parquet_encrypted_reference.cc

// Independent fixture oracle: decode with the unmodified Apache Arrow C++
// library shipped by PyArrow. Keys below are PUBLIC parquet-testing fixtures.
#include <array>
#include <bit>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string_view>
#include <unordered_map>
#include "arrow/filesystem/localfs.h"
#include "arrow/io/file.h"
#include "arrow/ipc/api.h"
#include "arrow/table.h"
#include "arrow/util/secure_string.h"
#include "parquet/arrow/reader.h"
#include "parquet/bloom_filter.h"
#include "parquet/bloom_filter_reader.h"
#include "parquet/column_reader.h"
#include "parquet/encryption/crypto_factory.h"
#include "parquet/encryption/kms_client_factory.h"
#include "parquet/file_reader.h"
#include "parquet/page_index.h"

// This exported library helper implements Apache's mock InMemoryKMS wrapping.
// Its declaration matches parquet/encryption/key_toolkit_internal.h; that
// internal header is not distributed in PyArrow's public include directory.
namespace parquet::encryption::internal {
::arrow::util::SecureString DecryptKeyLocally(const std::string&,
    const ::arrow::util::SecureString&, const std::string&);
}
using arrow::util::SecureString;
using Keys = std::unordered_map<std::string, SecureString>;

Keys FixtureKeys(bool wide) {
  Keys keys;
  keys.emplace("kf", SecureString(std::string(wide ? "01234567890123456789012345678901" : "0123456789012345")));
  for (int n = 1; n <= (wide ? 8 : 2); ++n) {
    auto key = wide ? "123456789012345678901234567890" + std::to_string(n + 11)
                    : "123456789012345" + std::to_string(n - 1);
    keys.emplace("kc" + std::to_string(n), SecureString(std::move(key)));
  }
  return keys;
}

class FixtureKms : public parquet::encryption::KmsClient {
  Keys keys_ = FixtureKeys(false);
 public:
  std::string WrapKey(const SecureString&, const std::string&) override {
    throw std::runtime_error("read-only fixture KMS");
  }
  SecureString UnwrapKey(const std::string& wrapped, const std::string& id) override {
    if (id == "wireform-test" && wrapped == "test-key") return SecureString(16, 0x42);
    if (id == "wireform-test") {
      for (int bytes : {16,24,32}) {
        if (wrapped == "test-key-" + std::to_string(bytes)) return SecureString(bytes, 0x42);
      }
    }
    return parquet::encryption::internal::DecryptKeyLocally(wrapped, keys_.at(id), id);
  }
};
class FixtureKmsFactory : public parquet::encryption::KmsClientFactory {
 public:
  std::shared_ptr<parquet::encryption::KmsClient> CreateKmsClient(
      const parquet::encryption::KmsConnectionConfig&) override {
    return std::make_shared<FixtureKms>();
  }
};

std::string Hex(const void* raw, size_t size) {
  const auto* data = static_cast<const uint8_t*>(raw);
  constexpr char digits[] = "0123456789abcdef";
  std::string out; out.reserve(size * 2);
  for (size_t i = 0; i < size; ++i) {
    out += digits[data[i] >> 4]; out += digits[data[i] & 15];
  }
  return out;
}

std::string JsonString(const std::string& text) {
  constexpr char hex[] = "0123456789abcdef";
  std::string out = "\"";
  for (uint8_t byte : text) {
    if (byte == '"' || byte == '\\') { out += '\\'; out += byte; }
    else if (byte < 32) { out += "\\u00"; out += hex[byte >> 4]; out += hex[byte & 15]; }
    else out += byte;
  }
  return out + '"';
}
template<class T> std::string PlainValue(T value, int) {
  static_assert(std::endian::native == std::endian::little);
  return std::string(reinterpret_cast<const char*>(&value), sizeof(T));
}
template<> std::string PlainValue(bool value, int) {
  return std::string(1, value ? '\1' : '\0');
}
template<> std::string PlainValue(parquet::ByteArray value, int) {
  return value.len ? std::string(reinterpret_cast<const char*>(value.ptr), value.len) : std::string();
}
template<> std::string PlainValue(parquet::FixedLenByteArray value, int width) {
  return std::string(reinterpret_cast<const char*>(value.ptr), width);
}

template<class Type> void Dump(parquet::ColumnReader* raw, std::ostream& out, const parquet::BloomFilter* bloom) {
  auto* reader = dynamic_cast<parquet::TypedColumnReader<Type>*>(raw);
  if (!reader) throw std::runtime_error("physical reader mismatch");
  const auto* schema = reader->descr();
  using T = typename Type::c_type;
  std::array<T, 128> values;
  std::array<int16_t, 128> defs{}, reps{};
  std::ostringstream definitions, repetitions, payloads;
  bool first_level = true, first_value = true;
  int64_t events = 0, present = 0;
  while (reader->HasNext()) {
    int64_t count = 0;
    auto levels = reader->ReadBatch(values.size(), defs.data(), reps.data(), values.data(), &count);
    if (levels <= 0) throw std::runtime_error("reader made no progress");
    for (int64_t i = 0; i < levels; ++i) {
      if (!first_level) { definitions << ','; repetitions << ','; }
      first_level = false;
      definitions << (schema->max_definition_level() ? defs[i] : 0);
      repetitions << (schema->max_repetition_level() ? reps[i] : 0);
    }
    for (int64_t i = 0; i < count; ++i) {
      if (!first_value) payloads << ',';
      first_value = false;
      const auto plain = PlainValue(values[i], schema->type_length());
      if (bloom && !bloom->FindHash(bloom->Hash(std::string_view(plain)))) {
        throw std::runtime_error("bloom false negative for an independently decoded value");
      }
      payloads << '\"' << Hex(plain.data(), plain.size()) << '\"';
    }
    events += levels; present += count;
  }
  out << ",\"bloomValuesChecked\":" << (bloom ? present : 0)
      << ",\"events\":" << events << ",\"present\":" << present
      << ",\"definitions\":[" << definitions.str() << "]"
      << ",\"repetitions\":[" << repetitions.str() << "]"
      << ",\"values\":[" << payloads.str() << "]";
}

int main(int argc, char** argv) {
 try {
  if (argc != 2 && argc != 3 && argc != 4) throw std::runtime_error("usage: reference <encrypted fixture> [arrow-output [native-forward|plaintext]]");
  const bool native_forward = argc == 4 && std::string(argv[3]) == "native-forward";
  const bool plaintext = argc == 4 && std::string(argv[3]) == "plaintext";
  const bool writer_features = argc == 4 && std::string(argv[3]).starts_with("writer-");
  if (argc == 4 && !native_forward && !plaintext && !writer_features) throw std::runtime_error("unknown fixture key configuration");
  const std::string path = argv[1];
  parquet::ReaderProperties properties;
  properties.set_page_checksum_verification(true);
  std::shared_ptr<parquet::FileDecryptionProperties> decryption;
  const bool external = path.find("external_key_material") != std::string::npos;
  if (writer_features) {
    const int width = std::stoi(std::string(argv[3]).substr(7));
    if (width != 16 && width != 24 && width != 32) throw std::runtime_error("invalid public fixture key width");
    auto retriever = std::make_shared<parquet::StringKeyIdRetriever>();
    retriever->PutKey("kf", SecureString(width, 0x42));
    retriever->PutKey("kc1", SecureString(width, 0x43));
    retriever->PutKey("kc2", SecureString(width, 0x44));
    parquet::FileDecryptionProperties::Builder builder;
    builder.key_retriever(retriever);
    builder.aad_prefix("writer-regression");
    decryption = builder.build();
  } else if (external || native_forward) {
    parquet::encryption::CryptoFactory factory;
    factory.RegisterKmsClientFactory(std::make_shared<FixtureKmsFactory>());
    parquet::encryption::KmsConnectionConfig connection;
    if (native_forward) connection.kms_instance_id = "wireform-test";
    parquet::encryption::DecryptionConfiguration config;
    decryption = factory.GetFileDecryptionProperties(connection, config, path,
        std::make_shared<arrow::fs::LocalFileSystem>());
  } else {
    auto retriever = std::make_shared<parquet::StringKeyIdRetriever>();
    for (const auto& [id, key] : FixtureKeys(path.find("aes256/") != std::string::npos)) {
      retriever->PutKey(id, key);
    }
    parquet::FileDecryptionProperties::Builder builder;
    builder.key_retriever(retriever);
    if (path.find("_aad") != std::string::npos) builder.aad_prefix("tester");
    decryption = builder.build();
  }
  if (!plaintext) properties.file_decryption_properties(decryption);
  auto file = parquet::ParquetFileReader::OpenFile(path, false, properties);
  auto metadata = file->metadata();
  std::ifstream input(path, std::ios::binary);
  const std::string raw((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
  if (raw.size() < 12) throw std::runtime_error("short fixture");
  auto length = static_cast<uint32_t>(static_cast<uint8_t>(raw[raw.size()-8]))
      | static_cast<uint32_t>(static_cast<uint8_t>(raw[raw.size()-7])) << 8
      | static_cast<uint32_t>(static_cast<uint8_t>(raw[raw.size()-6])) << 16
      | static_cast<uint32_t>(static_cast<uint8_t>(raw[raw.size()-5])) << 24;
  if (length > raw.size()-12) throw std::runtime_error("fixture footer out of bounds");
  parquet::EncryptionAlgorithm algorithm{};
  std::string footer_key_metadata;
  if (raw.substr(raw.size()-4) == "PARE") {
    auto crypto = parquet::FileCryptoMetaData::Make(
        reinterpret_cast<const uint8_t*>(raw.data()+raw.size()-8-length), static_cast<int64_t>(length));
    algorithm = crypto->encryption_algorithm(); footer_key_metadata = crypto->key_metadata();
  } else if (!plaintext) {
    algorithm = metadata->encryption_algorithm(); footer_key_metadata = metadata->footer_signing_key_metadata();
  }
  auto footer_key = decryption->key_retriever()->GetKey(plaintext ? "kf" : footer_key_metadata);
  auto indexes = file->GetPageIndexReader();
  std::cout << "{\"rows\":" << metadata->num_rows() << ",\"rowGroups\":"
            << metadata->num_row_groups() << ",\"columns\":" << metadata->num_columns()
            << ",\"fileIdHex\":\"" << Hex(algorithm.aad.aad_file_unique.data(), algorithm.aad.aad_file_unique.size())
            << "\",\"storedPrefixHex\":\"" << Hex(algorithm.aad.aad_prefix.data(), algorithm.aad.aad_prefix.size())
            << "\",\"prefixRequired\":" << (algorithm.aad.supply_aad_prefix ? "true" : "false")
            << ",\"algorithm\":" << algorithm.algorithm
            << ",\"footerKeyMetadataHex\":\"" << Hex(footer_key_metadata.data(), footer_key_metadata.size()) << "\""
            << ",\"publicFixtureFooterKeyHex\":\"" << Hex(footer_key.as_span().data(), footer_key.size()) << "\"}\n";
  for (int rg = 0; rg < metadata->num_row_groups(); ++rg) {
    auto group = file->RowGroup(rg);
    auto rg_indexes = indexes ? indexes->RowGroup(rg) : nullptr;
    auto rg_blooms = file->GetBloomFilterReader().RowGroup(rg);
    for (int col = 0; col < metadata->num_columns(); ++col) {
      auto reader = group->Column(col);
      auto* schema = reader->descr();
      auto ci = rg_indexes ? rg_indexes->GetColumnIndex(col) : nullptr;
      auto oi = rg_indexes ? rg_indexes->GetOffsetIndex(col) : nullptr;
      auto bloom = rg_blooms ? rg_blooms->GetColumnBloomFilter(col) : nullptr;
      auto crypto = metadata->RowGroup(rg)->ColumnChunk(col)->crypto_metadata();
      std::string key_metadata;
      SecureString column_key;
      if (crypto) {
        key_metadata = crypto->key_metadata();
        column_key = crypto->encrypted_with_footer_key() ? footer_key : decryption->key_retriever()->GetKey(key_metadata);
      }
      std::cout << "{\"rowGroup\":" << rg << ",\"column\":" << col
        << ",\"path\":\"" << schema->path()->ToDotString() << "\",\"type\":" << schema->physical_type()
        << ",\"maxDefinition\":" << schema->max_definition_level()
        << ",\"maxRepetition\":" << schema->max_repetition_level()
        << ",\"columnIndex\":" << (ci ? "true" : "false")
        << ",\"offsetPages\":" << (oi ? oi->page_locations().size() : 0)
        << ",\"bloomBytes\":" << (bloom ? bloom->GetBitsetSize() : 0);
      std::cout << ",\"encrypted\":" << (crypto ? "true" : "false")
        << ",\"keyMetadataHex\":\"" << Hex(key_metadata.data(), key_metadata.size())
        << "\",\"publicFixtureColumnKeyHex\":\"" << Hex(column_key.as_span().data(), column_key.size()) << "\",\"pathParts\":[";
      bool first_part = true;
      const auto schema_path = schema->path();
      for (const auto& part : schema_path->ToDotVector()) {
        if (!first_part) std::cout << ',';
        first_part = false;
        std::cout << JsonString(part);
      }
      std::cout << ']';
      std::cout << ",\"pageLocations\":[";
      bool first_page = true;
      if (oi) for (const auto& location : oi->page_locations()) {
        if (!first_page) std::cout << ',';
        first_page = false;
        std::cout << '[' << location.offset << ',' << location.compressed_page_size << ',' << location.first_row_index << ']';
      }
      std::cout << "],\"pageMaxima\":[";
      first_page = true;
      if (ci) for (const auto& value : ci->encoded_max_values()) {
        if (!first_page) std::cout << ',';
        first_page = false;
        std::cout << '"' << Hex(value.data(), value.size()) << '"';
      }
      std::cout << "],\"nullPages\":[";
      first_page = true;
      if (ci) for (bool value : ci->null_pages()) {
        if (!first_page) std::cout << ',';
        first_page = false;
        std::cout << (value ? "true" : "false");
      }
      std::cout << ']';
      switch (schema->physical_type()) {
        case parquet::Type::BOOLEAN: Dump<parquet::BooleanType>(reader.get(), std::cout, bloom.get()); break;
        case parquet::Type::INT32: Dump<parquet::Int32Type>(reader.get(), std::cout, bloom.get()); break;
        case parquet::Type::INT64: Dump<parquet::Int64Type>(reader.get(), std::cout, bloom.get()); break;
        case parquet::Type::INT96: Dump<parquet::Int96Type>(reader.get(), std::cout, bloom.get()); break;
        case parquet::Type::FLOAT: Dump<parquet::FloatType>(reader.get(), std::cout, bloom.get()); break;
        case parquet::Type::DOUBLE: Dump<parquet::DoubleType>(reader.get(), std::cout, bloom.get()); break;
        case parquet::Type::BYTE_ARRAY: Dump<parquet::ByteArrayType>(reader.get(), std::cout, bloom.get()); break;
        case parquet::Type::FIXED_LEN_BYTE_ARRAY: Dump<parquet::FLBAType>(reader.get(), std::cout, bloom.get()); break;
        default: throw std::runtime_error("unsupported physical type");
      }
      std::cout << "}\n";
    }
  }
  if (argc >= 3 && std::string(argv[2]) != "-") {
    // Keep raw INT96 comparisons in the physical oracle: converting the
    // deliberately extreme fixture values to Arrow timestamps can overflow.
    std::vector<int> columns;
    for (int i = 0; i < metadata->num_columns(); ++i) {
      if (metadata->schema()->Column(i)->physical_type() != parquet::Type::INT96) columns.push_back(i);
    }
    auto reader = parquet::arrow::FileReader::Make(arrow::default_memory_pool(), std::move(file)).ValueOrDie();
    auto table = reader->ReadTable(columns).ValueOrDie();
    auto sink = arrow::io::FileOutputStream::Open(argv[2]).ValueOrDie();
    auto output = arrow::ipc::MakeStreamWriter(sink, table->schema()).ValueOrDie();
    PARQUET_THROW_NOT_OK(output->WriteTable(*table));
    PARQUET_THROW_NOT_OK(output->Close());
    PARQUET_THROW_NOT_OK(sink->Close());
  }
  return 0;
 } catch (const std::exception& error) {
  std::cerr << error.what() << '\n'; return 1;
 }
}