wireform-parquet-0.1.0.0: scripts/parquet_encryption_interop.py
#!/usr/bin/env python3
"""Read Wireform's encrypted output with Apache Arrow's independent C++ reader.
Uses a deliberately public test key and a local mock KMS. Covers both page
versions, AES128/192/256, GCM/CTR, footer-only/selective column encryption,
and two row groups. Independent C++ metadata checks prove that requested
column encryption is present, in addition to checking decoded values.
No real KMS, account, or credentials are used.
"""
import argparse
import json
import subprocess
import tempfile
from pathlib import Path
import pyarrow as pa
import pyarrow.parquet as pq
import pyarrow.parquet.encryption as pe
class TestKms(pe.KmsClient):
def unwrap_key(self, wrapped_key, master_key_identifier):
widths = {"test-key": 16, "test-key-16": 16, "test-key-24": 24, "test-key-32": 32}
if master_key_identifier != "wireform-test" or wrapped_key not in widths:
raise ValueError("unexpected test key metadata")
return bytes([0x42]) * widths[wrapped_key]
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--probe", type=Path, required=True)
parser.add_argument("--reference", type=Path, required=True,
help="C++ fixture oracle built by build_parquet_encrypted_reference.py")
args = parser.parse_args()
factory = pe.CryptoFactory(lambda config: TestKms())
connection = pe.KmsConnectionConfig(kms_instance_id="wireform-test")
properties = factory.file_decryption_properties(connection)
schema = pa.schema([pa.field("x", pa.int32(), nullable=False), pa.field("y", pa.string(), nullable=False)])
expected = pa.table({"x": [1, 2, 3, 4, 5], "y": ["a", "b", "c", "d", "e"]}, schema=schema)
with tempfile.TemporaryDirectory(prefix="wireform-parquet-encryption-") as tmp:
root = Path(tmp)
for width in (16, 24, 32):
directory = root / str(width)
directory.mkdir()
metadata = directory / "test-key.json"
metadata.write_text(json.dumps({
"keyMaterialType": "PKMT1", "internalStorage": True,
"isFooterKey": True, "kmsInstanceID": "wireform-test", "kmsInstanceURL": "",
"masterKeyID": "wireform-test", "wrappedDEK": f"test-key-{width}",
"doubleWrapping": False,
}))
subprocess.run([str(args.probe.resolve()), "--encrypted", str(directory), str(metadata), str(width)], check=True)
fixtures = sorted(root.rglob("*.parquet"))
if len(fixtures) != 24:
raise AssertionError(f"expected 24 encryption fixtures, got {len(fixtures)}")
for path in fixtures:
assert path.read_bytes()[:4] == b"PARE", path.name
file = pq.ParquetFile(path, decryption_properties=properties)
assert file.num_row_groups == 2, path.name
table = file.read(use_threads=False)
if not table.equals(expected, check_metadata=False):
raise AssertionError(f"{path.name}: unexpected decoded values/types: {table}")
# Matching values alone cannot prove requested column encryption:
# a writer that ignores the option still round-trips through a
# reader with the footer key. Inspect independent C++ crypto
# metadata and read every page/index/bloom through that library.
arrow_output = root / (path.name + ".arrow")
reference = subprocess.run([str(args.reference.resolve()), str(path), str(arrow_output), "native-forward"],
capture_output=True, text=True, check=True)
header, *columns = map(json.loads, reference.stdout.splitlines())
assert header["rowGroups"] == 2, path.name
for column in columns:
requested = "-column" in path.name and column["column"] == 0
assert column["encrypted"] == requested, (path.name, column["column"], "column encryption option ignored")
assert column["columnIndex"] and column["offsetPages"] == 1, path.name
if column["column"] == 0:
assert column["bloomBytes"] > 0, path.name
independent = pa.ipc.open_stream(arrow_output).read_all()
assert independent.equals(expected, check_metadata=False), path.name
# Corrupt the final footer authentication-tag byte. Even files
# using CTR for pages must authenticate their footer with GCM.
tampered = bytearray(path.read_bytes())
tampered[-9] ^= 1
try:
pq.ParquetFile(pa.BufferReader(tampered), decryption_properties=properties)
except (OSError, pa.ArrowException):
pass
else:
raise AssertionError(f"{path.name}: accepted a tampered footer")
print(f"PASS {path.name}")
print(f"{len(fixtures)} encrypted files verified with PyArrow {pa.__version__}")
if __name__ == "__main__":
main()