net-kingdom/tools/bootstrap-recovery/retrieve-identity.py
tegwick 994d11c869
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
feat(recovery): prepare full identity backup and bootstrap handoff
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
2026-10-16 01:37:11 +02:00

71 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""Silent attended owner retrieval of an existing identity ciphertext.
Uses Backup-account recovery custody only in memory; no private age key, upload,
share, grant, deletion or production restore operation.
"""
import argparse
import base64
from datetime import datetime,timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import re
import tempfile
import urllib.request
HERE=Path(__file__).resolve().parent
def load(name):
spec=importlib.util.spec_from_file_location(name,HERE/(name+'.py'))
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m
inventory=load('inventory-storage-share');offsite=load('identity-offsite')
def retrieve(record,out):
if (record.get('schema')!='netkingdom.identity-offsite.v1' or record.get('status')!='readback_verified'
or not offsite.NAME.fullmatch(record.get('artifact','')) or not re.fullmatch('[a-f0-9]{64}',record.get('ciphertext_sha256',''))
or not 0<record.get('ciphertext_bytes',0)<=offsite.LIMIT):raise offsite.Refused()
identity,credentials=inventory.caller_credentials()
auth=base64.b64encode((credentials['BACKUP_USERNAME']+':'+credentials['BACKUP_PASSWORD']).encode()).decode()
url=offsite.HOST+'/remote.php/dav/files/Backup/railiance-backups/net-kingdom-identity/'+record['artifact']
h={'Authorization':'Basic '+auth,'Accept-Encoding':'identity'}
client=offsite.opener()
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response:
version=offsite.etag(response,url,record['ciphertext_bytes'])
if version!=record['etag']:raise offsite.Refused()
h['If-Match']=version
directory=Path(tempfile.mkdtemp(prefix='nk49-identity-fetched-',dir='/tmp'))
target=directory/record['artifact'];count=0;digest=hashlib.sha256()
with client.open(urllib.request.Request(url,headers=h),timeout=180) as response:
if offsite.etag(response,url,record['ciphertext_bytes'])!=version:raise offsite.Refused()
with target.open('xb') as output:
while block:=response.read(1024*1024):
count+=len(block)
if count>record['ciphertext_bytes']:raise offsite.Refused()
digest.update(block);output.write(block)
if count!=record['ciphertext_bytes'] or digest.hexdigest()!=record['ciphertext_sha256']:raise offsite.Refused()
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response:
if offsite.etag(response,url,record['ciphertext_bytes'])!=version:raise offsite.Refused()
out.update(record);out.update(status='readback_verified',fetched_path=str(target),
independent_owner_retrieval_verified=True,caller_entity_id=identity.get('entity_id'),
age_private_key_used=False,secret_values_exported=False,downloaded_at=datetime.now(timezone.utc).isoformat())
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--source-receipt',required=True);p.add_argument('--receipt',required=True)
a=p.parse_args();os.umask(0o077);fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'status':'failed','source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
try:retrieve(json.loads(Path(a.source_receipt).read_text()),r)
except Exception:r.update(status='failed',error='contained_owner_retrieval_failed')
finally:
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n')
return int(r['status']=='failed')
if __name__=='__main__':raise SystemExit(main())