Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a11fd9-caf6-7a63-b578-9daaaa134ff2
89 lines
4.1 KiB
Python
89 lines
4.1 KiB
Python
#!/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
|