#!/usr/bin/env python3 """Copy ciphertext into the isolated recovery project; never read an age key.""" import argparse import hashlib import json import os from pathlib import Path from ledger_archive import Refused, checkpoint, digest, private_file PROJECT = "1d37ea63-8c8f-44e1-80e1-2de862579ba7" BUCKET = "railiance-email-connect-archives" def mirror(client, archive, receipt): private_file(archive) expected = digest(archive) size = archive.stat().st_size if expected != receipt["ciphertext_sha256"] or size != receipt["ciphertext_bytes"]: raise Refused("mirror_source_differs") key = "archives/" + receipt["object_key"] try: with archive.open("rb") as source: created = client.put_object(Bucket=BUCKET, Key=key, Body=source, IfNoneMatch="*", Metadata={"sha256": expected}) version = created.get("VersionId") except Exception as error: if getattr(error, "response", {}).get("ResponseMetadata", {}).get("HTTPStatusCode") != 412: raise current = client.head_object(Bucket=BUCKET, Key=key) if current.get("Metadata", {}).get("sha256") != expected or current.get("ContentLength") != size: raise Refused("mirror_existing_object_differs") version = current.get("VersionId") if not version or version == "null": raise Refused("versioned_mirror_required") result = client.get_object(Bucket=BUCKET, Key=key, VersionId=version) hashed, count = hashlib.sha256(), 0 with result["Body"] as stream: while block := stream.read(1024**2): count += len(block) if count > size: raise Refused("mirror_readback_exceeds_size") hashed.update(block) if (result.get("VersionId") != version or count != size or hashed.hexdigest() != expected): raise Refused("mirror_readback_differs") # Recovery needs a receipt even if both node and workstation are lost. record = dict(receipt, recovery_mirror={"bucket": BUCKET, "object_key": key, "version_id": version, "project_id": PROJECT, "hash_readback_verified": True}) payload = json.dumps(record, sort_keys=True).encode() receipt_key = key + "." + version + "." + hashlib.sha256(payload).hexdigest() + ".receipt.json" try: saved = client.put_object(Bucket=BUCKET, Key=receipt_key, Body=payload, IfNoneMatch="*") receipt_version = saved.get("VersionId") except Exception as error: if getattr(error, "response", {}).get("ResponseMetadata", {}).get("HTTPStatusCode") != 412: raise receipt_version = client.head_object(Bucket=BUCKET, Key=receipt_key).get("VersionId") if not receipt_version or receipt_version == "null": raise Refused("versioned_mirror_receipt_required") saved = client.get_object(Bucket=BUCKET, Key=receipt_key, VersionId=receipt_version) with saved["Body"] as stream: if stream.read(len(payload) + 1) != payload: raise Refused("mirror_receipt_readback_differs") return dict(record["recovery_mirror"], receipt_key=receipt_key, receipt_version_id=receipt_version) def main(): import boto3 parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--archive", type=Path, required=True) parser.add_argument("--primary-receipt", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() private_file(args.primary_receipt) key = os.environ["AWS_ACCESS_KEY_ID"].split("@", 1)[0] + "@" + PROJECT client = boto3.client("s3", endpoint_url="https://s3.nl-ams.scw.cloud", region_name="nl-ams", aws_access_key_id=key, aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"]) checkpoint(args.output, mirror(client, args.archive, json.loads(args.primary_receipt.read_text()))) if __name__ == "__main__": try: main() except Exception as error: print(json.dumps({"status": "failed", "failure_type": type(error).__name__})) raise SystemExit(1) from None