Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m5s
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a11538-05ea-7b63-b3c7-a2df5f38898d
160 lines
6.1 KiB
Python
160 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Attended private decryption/unpacking; never modifies a running application."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import stat
|
|
import subprocess
|
|
import tarfile
|
|
from pathlib import Path
|
|
|
|
MAX_PACKET_BYTES = 20 * 1024**3
|
|
EXPECTED = {"database.dump", "content.tar.gz", "receipt.json"}
|
|
PREFIX = "s3://railiance-platform-pg-backup/platform-pg/application-archives/coulomb-social/"
|
|
|
|
|
|
class Refused(RuntimeError):
|
|
pass
|
|
|
|
|
|
def digest(path):
|
|
with path.open("rb") as stream:
|
|
return hashlib.file_digest(stream, "sha256").hexdigest()
|
|
|
|
|
|
def private_file(path):
|
|
info = path.lstat()
|
|
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.geteuid() or info.st_mode & 0o077:
|
|
raise Refused("private_owner_file_required")
|
|
|
|
|
|
def unpack_packet(packet: Path, destination: Path) -> dict:
|
|
"""Inspect the complete tar before writing any member; ignore stored modes."""
|
|
if packet.stat().st_size > MAX_PACKET_BYTES:
|
|
raise Refused("packet_too_large")
|
|
with tarfile.open(packet, "r:") as bundle:
|
|
entries = bundle.getmembers()
|
|
if (
|
|
len(entries) != len(EXPECTED)
|
|
or {e.name for e in entries} != EXPECTED
|
|
or any(not e.isfile() or e.sparse is not None for e in entries)
|
|
or sum(e.size for e in entries) > MAX_PACKET_BYTES
|
|
or any(e.size <= 0 or (e.name == "receipt.json" and e.size > 1024**2) for e in entries)
|
|
):
|
|
raise Refused("invalid_packet_members")
|
|
for entry in entries:
|
|
with (
|
|
bundle.extractfile(entry) as source,
|
|
(destination / entry.name).open("xb") as target,
|
|
):
|
|
os.chmod(target.name, 0o600)
|
|
remaining = entry.size
|
|
while remaining:
|
|
block = source.read(min(remaining, 8 * 1024**2))
|
|
if not block:
|
|
raise Refused("truncated_packet_member")
|
|
target.write(block)
|
|
remaining -= len(block)
|
|
receipt = json.loads((destination / "receipt.json").read_text())
|
|
if (
|
|
receipt.get("status") != "captured"
|
|
or not receipt.get("writers_quiesced")
|
|
or not receipt.get("app_resumed")
|
|
or receipt.get("database") != "coulomb_social_db"
|
|
):
|
|
raise Refused("pair_incomplete")
|
|
for name, prefix in [("database.dump", "database"), ("content.tar.gz", "content")]:
|
|
path = destination / name
|
|
if (
|
|
path.stat().st_size != receipt[prefix + "_bytes"]
|
|
or digest(path) != receipt[prefix + "_sha256"]
|
|
):
|
|
raise Refused("pair_hash_or_size_mismatch")
|
|
return receipt
|
|
|
|
|
|
def recover(archive: Path, transfer_receipt: Path, identity: Path, destination: Path) -> dict:
|
|
for path in [archive, transfer_receipt, identity]:
|
|
private_file(path)
|
|
transfer = json.loads(transfer_receipt.read_text())
|
|
encrypted_hash = transfer.get("encrypted_sha256", transfer.get("ciphertext_sha256"))
|
|
encrypted_bytes = transfer.get("encrypted_bytes", transfer.get("ciphertext_bytes"))
|
|
version = transfer.get("version_id")
|
|
if (
|
|
transfer.get("status") != "offsite_fetched_verified"
|
|
or transfer.get("application") != "coulomb-social"
|
|
or not transfer.get("remote_retrieval_verified")
|
|
or not isinstance(version, str)
|
|
or version in {"", "null"}
|
|
or "\n" in version
|
|
or not transfer.get("destination", "").startswith(PREFIX)
|
|
or not re.fullmatch("[a-f0-9]{64}", encrypted_hash or "")
|
|
or archive.stat().st_size != encrypted_bytes
|
|
or digest(archive) != encrypted_hash
|
|
):
|
|
raise Refused("verified_transfer_required")
|
|
if not destination.is_absolute() or destination.exists() or destination.is_symlink():
|
|
raise Refused("fresh_absolute_destination_required")
|
|
destination.mkdir(mode=0o700)
|
|
packet = destination / "packet.tar"
|
|
try:
|
|
result = subprocess.run(
|
|
["age", "-d", "-i", str(identity), "-o", str(packet), str(archive)],
|
|
capture_output=True,
|
|
timeout=900,
|
|
umask=0o077,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
raise Refused("decryption_timeout") from None
|
|
if result.returncode:
|
|
raise Refused("decryption_failed")
|
|
private_file(packet)
|
|
receipt = unpack_packet(packet, destination)
|
|
if receipt.get("started_at") != transfer.get("captured_at"):
|
|
raise Refused("capture_binding_mismatch")
|
|
# Root worker receipts also bind inner pair hashes and application image.
|
|
for key in ["image", "database_sha256", "content_sha256"]:
|
|
if key in transfer and transfer[key] != receipt.get(key):
|
|
raise Refused("application_binding_mismatch")
|
|
packet.unlink()
|
|
result = {
|
|
"status": "decrypted_pair_verified",
|
|
"version_id": version,
|
|
"encrypted_sha256": encrypted_hash,
|
|
"captured_at": receipt["started_at"],
|
|
"database_sha256": receipt["database_sha256"],
|
|
"content_sha256": receipt["content_sha256"],
|
|
"application_restore_verified": False,
|
|
}
|
|
with (destination / "unpack-receipt.json").open("x") as stream:
|
|
os.chmod(stream.name, 0o600)
|
|
json.dump(result, stream, indent=2)
|
|
return result
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--archive", type=Path, required=True)
|
|
parser.add_argument("--transfer-receipt", type=Path, required=True)
|
|
parser.add_argument("--identity", type=Path, required=True)
|
|
parser.add_argument("--destination", type=Path, required=True)
|
|
parser.add_argument("--execute", action="store_true")
|
|
args = parser.parse_args()
|
|
if not args.execute:
|
|
print(json.dumps({"status": "reviewed", "decryption_performed": False}))
|
|
return 0
|
|
print(json.dumps(recover(args.archive, args.transfer_receipt, args.identity, args.destination)))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
raise SystemExit(main())
|
|
except Exception as error:
|
|
print(json.dumps({"status": "failed", "failure_type": type(error).__name__}))
|
|
raise SystemExit(1)
|