83 lines
5 KiB
Python
83 lines
5 KiB
Python
#!/usr/bin/env python3
|
|
"""Synthetic clipboard on a private Wayland server; never log payloads.
|
|
|
|
Sensitive sentinels exist only in memory and must not reach the test DB.
|
|
Only aggregate counts and clipboard state labels are retained as evidence.
|
|
"""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import secrets
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
REPO=Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0,str(REPO/'tests/session_v2/fixtures'))
|
|
from headless import Lab
|
|
ART=Path(os.environ['STEP15A_ARTIFACTS']);ART.mkdir(mode=0o700,parents=True,exist_ok=True)
|
|
with Lab() as lab:
|
|
counter=lab.directory/'counts'
|
|
spy=lab.directory/'spy.py'
|
|
spy.write_text('import os,sys\nsys.stdin.buffer.read()\nwith open(os.environ["TEST_COUNTS"],"a") as f:f.write(sys.argv[1]+" "+os.environ.get("CLIPBOARD_STATE","")+"\\n")\n')
|
|
lab.env['TEST_COUNTS']=str(counter)
|
|
def copy(payload,kind='text/plain',sensitive=False):
|
|
p=subprocess.Popen(['wl-copy','--foreground','--type',kind,*(['--sensitive'] if sensitive else [])],env=lab.env,stdin=subprocess.PIPE,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,start_new_session=True)
|
|
lab.children.append(p);p.stdin.write(payload);p.stdin.close();time.sleep(.2)
|
|
def stop(procs):
|
|
for p in procs:
|
|
if p.poll() is None:
|
|
os.killpg(p.pid,signal.SIGTERM);p.wait(timeout=3)
|
|
png=subprocess.run(['magick','-size','2x2','xc:blue','png:-'],capture_output=True,env=lab.env,check=True).stdout
|
|
phases=[]
|
|
for mode in ['legacy','disjoint']:
|
|
before=len(counter.read_text().splitlines()) if counter.exists() else 0
|
|
watchers=[lab.start(['wl-paste',*(['--type','text'] if mode=='disjoint' else []),'--watch','python3',str(spy),'general']),lab.start(['wl-paste','--type','image/png','--watch','python3',str(spy),'image'])]
|
|
time.sleep(.2)
|
|
# Ignore the already-owned previous selection replayed to new watchers.
|
|
base=len(counter.read_text().splitlines()) if counter.exists() else 0
|
|
copy(png,'image/png');time.sleep(.2)
|
|
entries=counter.read_text().splitlines()[base:]
|
|
counts={name:sum(e.startswith(name+' ') for e in entries) for name in ['general','image']}
|
|
phases.append(dict(mode=mode,offer='PNG-only',callbacks=counts))
|
|
assert counts=={'general':0,'image':1},counts
|
|
# A data-control owner can offer both MIME types without focus/input.
|
|
protocol='/usr/share/qt6/wayland/protocols/wlr-data-control/wlr-data-control-unstable-v1.xml'
|
|
header=lab.directory/'data-control.h';code=lab.directory/'data-control.c'
|
|
subprocess.run(['wayland-scanner','client-header',protocol,str(header)],check=True)
|
|
subprocess.run(['wayland-scanner','private-code',protocol,str(code)],check=True)
|
|
provider=lab.directory/'mime-provider'
|
|
subprocess.run(['cc','-std=c17','-Wall','-Wextra','-I',str(lab.directory),str(REPO/'tests/step15a/mime-provider.c'),str(code),'-lwayland-client','-o',str(provider)],check=True)
|
|
base=len(counter.read_text().splitlines())
|
|
owner=lab.start([str(provider)]);time.sleep(.4)
|
|
entries=counter.read_text().splitlines()[base:]
|
|
counts={name:sum(e==name+' data' for e in entries) for name in ['general','image']}
|
|
assert counts=={'general':1,'image':1},counts
|
|
phases.append(dict(mode=mode,offer='text-and-PNG',callbacks=counts))
|
|
stop([owner])
|
|
stop(watchers)
|
|
subprocess.run(['wl-copy','--clear'],env=lab.env,check=True)
|
|
db=lab.directory/'db'
|
|
watchers=[lab.start(['wl-paste','--type','text','--watch','cliphist','-db-path',str(db),'store'])]
|
|
time.sleep(.15)
|
|
def stored_count():
|
|
p=subprocess.run(['cliphist','-db-path',str(db),'list'],env=lab.env,capture_output=True)
|
|
return len(p.stdout.splitlines()) if p.returncode==0 else 0
|
|
copy(b'TEST_ONLY ordinary');time.sleep(.2);assert stored_count()==1
|
|
# Random synthetic sensitive sentinel is never printed or written by tests.
|
|
sensitive=b'TEST_ONLY-'+secrets.token_bytes(32).hex().encode()
|
|
copy(sensitive,sensitive=True);time.sleep(.25);assert stored_count()==1
|
|
stop(watchers)
|
|
watchers=[lab.start(['wl-paste','--type','image/png','--watch','cliphist','-db-path',str(db),'store'])]
|
|
time.sleep(.15)
|
|
copy(png,'image/png');time.sleep(.2);assert stored_count()==2
|
|
copy(png,'image/png',sensitive=True);time.sleep(.2);assert stored_count()==2
|
|
stop(watchers)
|
|
# Also exercise the backend marker directly, independent of Wayland timing.
|
|
absent=lab.directory/'sensitive-only-db'
|
|
subprocess.run(['cliphist','-db-path',str(absent),'store'],input=sensitive,env=dict(lab.env,CLIPBOARD_STATE='sensitive'),check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
|
assert not absent.exists()
|
|
result=dict(overlap=phases,sensitive_wayland_text_retained=False,sensitive_wayland_png_retained=False,sensitive_direct_db_created=False,
|
|
real_clipboard_access=False,lab=str(lab.directory))
|
|
(ART/'clipboard-metrics.json').write_text(json.dumps(result,indent=2));print(json.dumps(result))
|