#!/usr/bin/env python3 """Build a secret-free offline recovery kit; optionally publish exact new files. Publication creates only three fixed versioned objects beside identity backups, never changes shares, credentials, existing files, retention or runtime jobs. """ import argparse import base64 from datetime import datetime, timezone import hashlib import importlib.util import io import json import os from pathlib import Path import sys import urllib.error import urllib.request import zipfile ROOT = Path(__file__).resolve().parents[2] HERE = Path(__file__).resolve().parent spec = importlib.util.spec_from_file_location('offline', HERE / 'retrieve-identity-offline.py') offline = importlib.util.module_from_spec(spec) spec.loader.exec_module(offline) offsite = offline.offsite FILES = [ 'docs/disaster-recovery-storage-share.md', 'docs/identity-service-restore-packet.md', 'docs/evidence/2026-10-16-identity-backup-live/actual-offsite.json', 'docs/evidence/2026-10-16-identity-outage-recovery/image-observation.json', 'tools/bootstrap-recovery/retrieve-identity-offline.py', 'tools/bootstrap-recovery/identity-offsite.py', 'tools/bootstrap-recovery/verify-identity.py', 'tools/bootstrap-recovery/identity-bundle.py', 'tools/bootstrap-recovery/check-privacyidea-database.py', 'tools/bootstrap-recovery/identity-backup-profile.json', 'sso-mfa/bootstrap/sops-custody-unlock.sh', 'sso-mfa/bootstrap/sops-private.py', 'keys/age.pub', ] def build(): content = {name: (ROOT / name).read_bytes() for name in FILES} if any(b'AGE-SECRET-KEY-1' in value for name, value in content.items() if not name.endswith(('.py', '.sh', '.md'))): raise offsite.Refused() sums = ''.join(hashlib.sha256(value).hexdigest() + ' ' + name + '\n' for name, value in content.items()).encode() output = io.BytesIO() with zipfile.ZipFile(output, 'w', compression=zipfile.ZIP_DEFLATED) as archive: for name, value in {**content, 'SHA256SUMS': sums}.items(): entry = zipfile.ZipInfo(name, date_time=(2026, 10, 16, 0, 0, 0)) entry.external_attr = 0o100600 << 16 entry.compress_type = zipfile.ZIP_DEFLATED archive.writestr(entry, value) kit = output.getvalue() version = hashlib.sha256(kit).hexdigest() stem = 'net-kingdom-recovery-' + version[:16] return { stem + '-START-HERE.md': content['docs/disaster-recovery-storage-share.md'], stem + '.zip': kit, stem + '.zip.sha256': (version + ' ' + stem + '.zip\n').encode(), } def publish(files, headers, client=None): client = client or offsite.opener() result = [] for name, raw in files.items(): url = offline.ACCOUNT_BASE + name digest = hashlib.sha256(raw).hexdigest() # A retry may accept an identical object after exact readback, but must # never overwrite a conflicting object or retry an unknown PUT outcome. present = False try: with client.open(urllib.request.Request(url, method='HEAD', headers=headers), timeout=30) as response: version = offsite.etag(response, url, len(raw)) present = True except urllib.error.HTTPError as error: if error.code != 404: raise if not present: h = dict(headers, **{'If-None-Match': '*', 'Content-Length': str(len(raw)), 'Content-Type': 'application/zip' if name.endswith('.zip') else 'text/plain; charset=utf-8'}) with client.open(urllib.request.Request(url, data=raw, method='PUT', headers=h), timeout=60) as response: if response.status != 201 or response.url != url: raise offsite.Refused() with client.open(urllib.request.Request(url, method='HEAD', headers=headers), timeout=30) as response: version = offsite.etag(response, url, len(raw)) h = dict(headers, **{'If-Match': version}) with client.open(urllib.request.Request(url, headers=h), timeout=60) as response: if offsite.etag(response, url, len(raw)) != version: raise offsite.Refused() downloaded = response.read(len(raw) + 1) if downloaded != raw: raise offsite.Refused() result.append({'name': name, 'bytes': len(raw), 'sha256': digest, 'etag': version, 'readback_verified': True}) return result def main(): p = argparse.ArgumentParser(description=__doc__) p.add_argument('--output-dir', required=True) p.add_argument('--publish', action='store_true') p.add_argument('--auth-source', choices=('prompt', 'attended-openbao'), default='prompt') p.add_argument('--receipt', required=True) p.add_argument('--offline-transfer-receipt', help='After prompted publication, retrieve the known recovered generation using the same independently held access') 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 = {'schema': 'netkingdom.recovery-kit.v1', 'status': 'failed', 'secret_values_exported': False, 'new_grants_created': False} stage = 'option_validation' try: if a.offline_transfer_receipt and (not a.publish or a.auth_source != 'prompt'): raise offsite.Refused() stage = 'build_kit' files = build() stage = 'write_local_kit' directory = Path(a.output_dir) directory.mkdir(mode=0o700, exist_ok=False) for name, raw in files.items(): (directory / name).write_bytes(raw) r.update(status='built', files=[{'name': n, 'bytes': len(v), 'sha256': hashlib.sha256(v).hexdigest()} for n, v in files.items()]) if a.publish: if a.auth_source == 'prompt': stage = 'private_provider_prompt' secret = offline.private_prompt('Independently held Storage Share Backup app password: ') else: stage = 'attended_owner_custody' spec = importlib.util.spec_from_file_location('inventory', HERE / 'inventory-storage-share.py') owner = importlib.util.module_from_spec(spec) spec.loader.exec_module(owner) _, credentials = owner.caller_credentials() secret = credentials['BACKUP_PASSWORD'] if not secret or any(c in secret for c in '\r\n\x00'): raise offsite.Refused() try: headers = {'Authorization': 'Basic ' + base64.b64encode(('Backup:' + secret).encode()).decode(), 'Accept-Encoding': 'identity'} stage = 'provider_publication' r.update(status='published_readback_verified', files=publish(files, headers), folder='Backup/railiance-backups/net-kingdom-identity', provider='Storage Share', credential_source=a.auth_source) if a.offline_transfer_receipt: stage = 'offline_provider_download' transfer_fd = os.open(a.offline_transfer_receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) transfer = {'status': 'failed', 'error': 'offline_retrieval_failed_private_diagnostics_suppressed'} try: record = json.loads((ROOT / FILES[2]).read_text()) transfer = offline.retrieve(record, secret, 'account') finally: with os.fdopen(transfer_fd, 'w') as out: json.dump(transfer, out, indent=2) out.write('\n') r['offline_retrieval_verified'] = transfer['status'] == 'readback_verified' finally: secret = None r['finished_at'] = datetime.now(timezone.utc).isoformat() except (Exception, KeyboardInterrupt) as error: r.update(status='failed', error='kit_operation_failed_private_diagnostics_suppressed', failed_stage=stage, **offline.failure_metadata(error)) finally: with os.fdopen(fd, 'w') as output: json.dump(r, output, indent=2) output.write('\n') # Silent: safe to use in the existing captured attended-owner conduit. return int(r['status'] == 'failed') if __name__ == '__main__': sys.exit(main())