Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
164 lines
7.4 KiB
Python
164 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Download one reviewed ciphertext using privately entered provider access.
|
|
|
|
No OpenBao, SSO, Kubernetes, credential discovery, provider writes or age key.
|
|
Run by the operator in their own terminal, never through a recorded agent PTY.
|
|
"""
|
|
import argparse
|
|
import base64
|
|
from datetime import datetime, timezone
|
|
import getpass
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import signal
|
|
import sys
|
|
import tempfile
|
|
import urllib.request
|
|
import urllib.error
|
|
import warnings
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
spec = importlib.util.spec_from_file_location('offsite', HERE / 'identity-offsite.py')
|
|
offsite = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(offsite)
|
|
ACCOUNT_BASE = offsite.HOST + '/remote.php/dav/files/Backup/railiance-backups/net-kingdom-identity/'
|
|
|
|
|
|
def private_prompt(label):
|
|
# getpass opens the terminal with os.open, which does not require seeking.
|
|
# Reject redirected input and turn its echoing fallback warning into an
|
|
# exception before it can read a credential without echo suppression.
|
|
if not sys.stdin.isatty() or not sys.stderr.isatty():
|
|
raise offsite.Refused()
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter('error', getpass.GetPassWarning)
|
|
return getpass.getpass(label)
|
|
|
|
|
|
def failure_metadata(error):
|
|
# No exception messages, response bodies, URLs or headers enter receipts.
|
|
if isinstance(error, urllib.error.HTTPError):
|
|
return {'failure_kind': 'provider_http_error', 'http_status': int(error.code)}
|
|
if isinstance(error, urllib.error.URLError):
|
|
return {'failure_kind': 'provider_transport_error'}
|
|
if isinstance(error, getpass.GetPassWarning):
|
|
return {'failure_kind': 'terminal_echo_suppression_unavailable'}
|
|
if isinstance(error, offsite.Refused):
|
|
return {'failure_kind': 'scope_or_integrity_refusal'}
|
|
return {'failure_kind': 'local_operation_failed'}
|
|
|
|
|
|
def validate(record):
|
|
if (record.get('schema') != 'netkingdom.identity-offsite.v1'
|
|
or record.get('status') != 'readback_verified'
|
|
or record.get('provider') != 'Storage Share'
|
|
or record.get('folder') != 'Backup/railiance-backups/net-kingdom-identity'
|
|
or not offsite.NAME.fullmatch(record.get('artifact', ''))
|
|
or not re.fullmatch('[a-f0-9]{64}', record.get('ciphertext_sha256', ''))
|
|
or type(record.get('ciphertext_bytes')) is not int
|
|
or not 0 < record['ciphertext_bytes'] <= offsite.LIMIT
|
|
or not re.fullmatch(r'"[^"\x00-\x20]{1,180}"', record.get('etag', ''))):
|
|
raise offsite.Refused()
|
|
|
|
|
|
def retrieve(record, secret, mode, client=None):
|
|
validate(record)
|
|
if mode not in {'account', 'folder-share'} or not secret or any(c in secret for c in '\r\n\x00'):
|
|
raise offsite.Refused()
|
|
auth = offsite.headers(secret) if mode == 'folder-share' else {
|
|
'Authorization': 'Basic ' + base64.b64encode(('Backup:' + secret).encode()).decode(),
|
|
'Accept-Encoding': 'identity'}
|
|
url = (offsite.BASE if mode == 'folder-share' else ACCOUNT_BASE) + record['artifact']
|
|
client = client or offsite.opener()
|
|
directory = None
|
|
complete = False
|
|
try:
|
|
with client.open(urllib.request.Request(url, method='HEAD', headers=auth), timeout=30) as response:
|
|
if offsite.etag(response, url, record['ciphertext_bytes']) != record['etag']:
|
|
raise offsite.Refused()
|
|
directory = Path(tempfile.mkdtemp(prefix='nk49-identity-fetched-', dir='/tmp'))
|
|
target = directory / record['artifact']
|
|
auth['If-Match'] = record['etag']
|
|
count = 0
|
|
digest = hashlib.sha256()
|
|
with client.open(urllib.request.Request(url, headers=auth), timeout=180) as response:
|
|
if offsite.etag(response, url, record['ciphertext_bytes']) != record['etag']:
|
|
raise offsite.Refused()
|
|
fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
|
with os.fdopen(fd, 'wb') 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)
|
|
output.flush()
|
|
os.fsync(output.fileno())
|
|
if count != record['ciphertext_bytes'] or digest.hexdigest() != record['ciphertext_sha256']:
|
|
raise offsite.Refused()
|
|
with client.open(urllib.request.Request(url, method='HEAD', headers=auth), timeout=30) as response:
|
|
if offsite.etag(response, url, record['ciphertext_bytes']) != record['etag']:
|
|
raise offsite.Refused()
|
|
complete = True
|
|
# Closed allowlist: never copy arbitrary source-receipt fields to output.
|
|
result = {k: record[k] for k in ('schema', 'provider', 'folder', 'artifact',
|
|
'ciphertext_bytes', 'ciphertext_sha256', 'etag')}
|
|
result.update(status='readback_verified', fetched_path=str(target),
|
|
independent_owner_retrieval_verified=True,
|
|
credential_source='independently_held_provider_access',
|
|
provider_auth_mode=mode, openbao_contacted=False, sso_contacted=False,
|
|
provider_writes_performed=False, age_private_key_used=False,
|
|
secret_values_exported=False, object_unchanged_during_transfer=True,
|
|
downloaded_at=datetime.now(timezone.utc).isoformat())
|
|
return result
|
|
finally:
|
|
if directory is not None and not complete:
|
|
shutil.rmtree(directory)
|
|
|
|
|
|
def interrupted(signum, frame):
|
|
raise InterruptedError()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--source-receipt', required=True)
|
|
parser.add_argument('--receipt', required=True)
|
|
parser.add_argument('--auth', choices=('account', 'folder-share'), default='account')
|
|
args = parser.parse_args()
|
|
os.umask(0o077)
|
|
fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
|
result = {'status': 'failed', 'error': 'offline_retrieval_failed_private_diagnostics_suppressed'}
|
|
stage = 'source_receipt_validation'
|
|
try:
|
|
record = json.loads(Path(args.source_receipt).read_text())
|
|
validate(record)
|
|
# getpass must never fall back to echoed stdin (e.g. a captured agent tool).
|
|
stage = 'private_provider_prompt'
|
|
secret = private_prompt('Storage Share Backup app password or exact folder-share token: ')
|
|
try:
|
|
stage = 'provider_download'
|
|
result = retrieve(record, secret, args.auth)
|
|
finally:
|
|
secret = None
|
|
except (Exception, KeyboardInterrupt) as error:
|
|
result.update(failed_stage=stage, **failure_metadata(error))
|
|
finally:
|
|
with os.fdopen(fd, 'w') as output:
|
|
json.dump(result, output, indent=2)
|
|
output.write('\n')
|
|
ok = result['status'] == 'readback_verified'
|
|
print('OK: ciphertext retrieved without OpenBao or SSO; ETag, size and SHA256 verified.' if ok
|
|
else 'ERROR: retrieval refused; private diagnostics suppressed. Use a new receipt path to retry.')
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == '__main__':
|
|
for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
|
|
signal.signal(sig, interrupted)
|
|
sys.exit(main())
|