Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
106 lines
6.4 KiB
Python
106 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Separate folder-scoped read/delete executor for verified new identity backups.
|
|
|
|
Unknown/unindexed files are never eligible. The recovered point, newest point,
|
|
seven daily and two weekly points remain protected. No grant or account login.
|
|
"""
|
|
from datetime import datetime,timezone
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import tempfile
|
|
import urllib.parse
|
|
import urllib.request
|
|
import xml.etree.ElementTree as ET
|
|
|
|
spec=importlib.util.spec_from_file_location('offsite',Path(__file__).with_name('identity-offsite.py'))
|
|
offsite=importlib.util.module_from_spec(spec);spec.loader.exec_module(offsite)
|
|
|
|
|
|
def listing(client,token):
|
|
h=offsite.headers(token);h.update({'Depth':'1','Content-Type':'application/xml'})
|
|
body=b'<d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/><d:getcontentlength/><d:getetag/></d:prop></d:propfind>'
|
|
with client.open(urllib.request.Request(offsite.BASE,data=body,method='PROPFIND',headers=h),timeout=30) as response:
|
|
if response.status!=207 or response.url!=offsite.BASE:raise offsite.Refused()
|
|
raw=response.read(4*1024*1024+1)
|
|
if len(raw)>4*1024*1024:raise offsite.Refused()
|
|
result={}
|
|
for response in ET.fromstring(raw).findall('{DAV:}response'):
|
|
href=urllib.parse.urlsplit(response.findtext('{DAV:}href') or '')
|
|
if href.netloc and href.netloc!=urllib.parse.urlsplit(offsite.HOST).netloc:raise offsite.Refused()
|
|
name=urllib.parse.unquote(href.path.rstrip('/').rsplit('/',1)[-1])
|
|
for row in response.findall('{DAV:}propstat'):
|
|
if ' 200 ' not in (row.findtext('{DAV:}status') or ''):continue
|
|
p=row.find('{DAV:}prop')
|
|
if p is None or p.find('{DAV:}resourcetype/{DAV:}collection') is not None:continue
|
|
is_index=name.endswith('.receipt.json') and offsite.NAME.fullmatch(name.removesuffix('.receipt.json'))
|
|
if not offsite.NAME.fullmatch(name) and not is_index:continue
|
|
size=p.findtext('{DAV:}getcontentlength') or '';version=p.findtext('{DAV:}getetag') or ''
|
|
if not size.isdigit() or not re.fullmatch(r'"[^"\x00-\x20]{1,180}"',version) or name in result:raise offsite.Refused()
|
|
result[name]={'bytes':int(size),'etag':version}
|
|
if len(result)>10000:raise offsite.Refused()
|
|
return result
|
|
|
|
|
|
def generations(client,token,objects):
|
|
result=[]
|
|
for name,meta in objects.items():
|
|
if not offsite.NAME.fullmatch(name) or name+'.receipt.json' not in objects:continue
|
|
index=objects[name+'.receipt.json']
|
|
if not 0<index['bytes']<=65536:raise offsite.Refused()
|
|
url=offsite.BASE+name+'.receipt.json';h=offsite.headers(token);h['If-Match']=index['etag']
|
|
with client.open(urllib.request.Request(url,headers=h),timeout=30) as response:
|
|
if offsite.etag(response,url,index['bytes'])!=index['etag']:raise offsite.Refused()
|
|
raw=response.read(65537)
|
|
if len(raw)!=index['bytes']:raise offsite.Refused()
|
|
r=json.loads(raw)
|
|
if (r.get('schema')!='netkingdom.identity-offsite-index.v1' or r.get('status')!='readback_verified'
|
|
or r.get('artifact')!=name or r.get('ciphertext_bytes')!=meta['bytes'] or not 0<meta['bytes']<=offsite.LIMIT
|
|
or not re.fullmatch('[a-f0-9]{64}',r.get('ciphertext_sha256',''))):raise offsite.Refused()
|
|
stamp=datetime.strptime(name.removeprefix('net-kingdom-identity-')[:16],'%Y%m%dT%H%M%SZ').replace(tzinfo=timezone.utc)
|
|
captured=datetime.fromisoformat(r['captured_at'])
|
|
if captured.tzinfo is None or abs((captured-stamp).total_seconds())>3600:raise offsite.Refused()
|
|
if stamp>datetime.now(timezone.utc):raise offsite.Refused()
|
|
result.append({**r,'stamp':stamp,'etag':meta['etag'],'index_etag':index['etag']})
|
|
return sorted(result,key=lambda r:r['stamp'],reverse=True)
|
|
|
|
|
|
def select(rows,protected):
|
|
if not rows or protected not in {r['artifact'] for r in rows}:raise offsite.Refused()
|
|
keep={protected,rows[0]['artifact']};days=set();weeks=set()
|
|
for row in rows:
|
|
day=row['stamp'].date();week=row['stamp'].isocalendar()[:2]
|
|
if day not in days and len(days)<7:keep.add(row['artifact']);days.add(day)
|
|
if week not in weeks and len(weeks)<2:keep.add(row['artifact']);weeks.add(week)
|
|
return [r for r in rows if r['artifact'] not in keep],keep
|
|
|
|
|
|
def run(client,token,accepted,receipt,execute=False,checkpoint=lambda:None):
|
|
if (accepted.get('status')!='identity_archive_verified' or accepted.get('container_cleanup_verified') is not True
|
|
or accepted.get('temporary_plaintext_cleanup_verified') is not True or not offsite.NAME.fullmatch(accepted.get('artifact',''))):raise offsite.Refused()
|
|
objects=listing(client,token);rows=generations(client,token,objects)
|
|
candidates,keep=select(rows,accepted['artifact'])
|
|
recovered=next(r for r in rows if r['artifact']==accepted['artifact'])
|
|
if recovered['ciphertext_sha256']!=accepted['ciphertext_sha256']:raise offsite.Refused()
|
|
receipt.update(status='retention_preview',protected=accepted['artifact'],keep=sorted(keep),
|
|
candidates=[r['artifact'] for r in candidates],deleted=[],unknown_objects_untouched=True)
|
|
checkpoint()
|
|
# Validate retained known-good and newest ciphertext before any deletion.
|
|
with tempfile.TemporaryDirectory(prefix='nk49-retained-check-',dir='/tmp') as temporary:
|
|
for index,row in enumerate({r['artifact']:r for r in [recovered,rows[0]]}.values()):
|
|
offsite.readback(client,token,row['artifact'],row['ciphertext_bytes'],row['ciphertext_sha256'],Path(temporary)/str(index))
|
|
if not execute:return
|
|
for index,row in enumerate(candidates):
|
|
version=offsite.readback(client,token,row['artifact'],row['ciphertext_bytes'],row['ciphertext_sha256'],Path(temporary)/('expire-'+str(index)))
|
|
for name,etag in [(row['artifact'],version),(row['artifact']+'.receipt.json',row['index_etag'])]:
|
|
h=offsite.headers(token);h['If-Match']=etag
|
|
receipt['deletion_in_progress']=name
|
|
checkpoint()
|
|
with client.open(urllib.request.Request(offsite.BASE+name,method='DELETE',headers=h),timeout=30) as response:
|
|
if response.status!=204 or response.url!=offsite.BASE+name:raise offsite.Refused()
|
|
receipt['deleted'].append(name)
|
|
checkpoint()
|
|
receipt.pop('deletion_in_progress',None)
|
|
receipt['status']='retention_applied'
|