From d5ae07973ab1a41ed16c1761a6f8aad0919d6582 Mon Sep 17 00:00:00 2001 From: everbarry Date: Thu, 7 May 2026 18:39:39 +0200 Subject: [PATCH 1/4] polish and self-contain file-share demo UI Co-authored-by: Cursor --- .github/workflows/fuzz-smoke.yml | 25 + cli/zkac_node.egg-info/SOURCES.txt | 2 + cli/zkac_node.egg-info/entry_points.txt | 1 + demo/README.md | 41 + .../cli_web_server.cpython-314.pyc | Bin 0 -> 16822 bytes .../file_share_client.cpython-314.pyc | Bin 0 -> 36061 bytes .../file_share_credentials.cpython-314.pyc | Bin 0 -> 19426 bytes .../file_share_server.cpython-314.pyc | Bin 0 -> 39288 bytes .../file_share_smoke.cpython-314.pyc | Bin 0 -> 15721 bytes .../file_share_tui.cpython-314.pyc | Bin 0 -> 70590 bytes demo/__pycache__/server.cpython-314.pyc | Bin 0 -> 43067 bytes .../simple_cli_client.cpython-314.pyc | Bin 0 -> 8230 bytes .../simple_i2p_server.cpython-314.pyc | Bin 0 -> 6746 bytes .../trustless_server.cpython-314.pyc | Bin 0 -> 16206 bytes .../zkac_admin_serve.cpython-314.pyc} | Bin 10744 -> 13634 bytes .../zkac_cli_adapter.cpython-314.pyc | Bin 0 -> 27584 bytes demo/creds/.gitignore | 2 - demo/file_share_client.py | 551 +++++++++ demo/file_share_credentials.py | 321 ++++++ demo/file_share_server.py | 653 +++++++++++ demo/file_share_smoke.py | 252 +++++ demo/file_share_tui.py | 1003 +++++++++++++++++ ...42f5e3b57ae0b73c8b99a4e0cafc9c3eedab.state | Bin 0 -> 628 bytes ...63ea5bc43228a3cf1d5f64a043994951c31ea.cert | Bin 0 -> 336 bytes ...3ea5bc43228a3cf1d5f64a043994951c31ea.state | Bin 0 -> 628 bytes ...b92c0ad7469322dcef8269bc8694ecf262386.cert | Bin 0 -> 336 bytes ...92c0ad7469322dcef8269bc8694ecf262386.state | Bin 0 -> 488 bytes demo/zkac_cli_adapter.py | 420 +++++++ pyproject.toml | 1 + uv.lock | 91 ++ 30 files changed, 3361 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/fuzz-smoke.yml create mode 100644 demo/README.md create mode 100644 demo/__pycache__/cli_web_server.cpython-314.pyc create mode 100644 demo/__pycache__/file_share_client.cpython-314.pyc create mode 100644 demo/__pycache__/file_share_credentials.cpython-314.pyc create mode 100644 demo/__pycache__/file_share_server.cpython-314.pyc create mode 100644 demo/__pycache__/file_share_smoke.cpython-314.pyc create mode 100644 demo/__pycache__/file_share_tui.cpython-314.pyc create mode 100644 demo/__pycache__/server.cpython-314.pyc create mode 100644 demo/__pycache__/simple_cli_client.cpython-314.pyc create mode 100644 demo/__pycache__/simple_i2p_server.cpython-314.pyc create mode 100644 demo/__pycache__/trustless_server.cpython-314.pyc rename demo/{zkac_admin_serve.py => __pycache__/zkac_admin_serve.cpython-314.pyc} (52%) create mode 100644 demo/__pycache__/zkac_cli_adapter.cpython-314.pyc delete mode 100644 demo/creds/.gitignore create mode 100644 demo/file_share_client.py create mode 100644 demo/file_share_credentials.py create mode 100644 demo/file_share_server.py create mode 100644 demo/file_share_smoke.py create mode 100644 demo/file_share_tui.py create mode 100644 demo/fs_data/registries/519bc59c917f122245d5a6a131ec42f5e3b57ae0b73c8b99a4e0cafc9c3eedab.state create mode 100644 demo/fs_data/registries/a0e5ed90dafc9ddfa18197b599a63ea5bc43228a3cf1d5f64a043994951c31ea.cert create mode 100644 demo/fs_data/registries/a0e5ed90dafc9ddfa18197b599a63ea5bc43228a3cf1d5f64a043994951c31ea.state create mode 100644 demo/fs_data/registries/fc2c7a6c4a57247946f5bcf006fb92c0ad7469322dcef8269bc8694ecf262386.cert create mode 100644 demo/fs_data/registries/fc2c7a6c4a57247946f5bcf006fb92c0ad7469322dcef8269bc8694ecf262386.state create mode 100644 demo/zkac_cli_adapter.py diff --git a/.github/workflows/fuzz-smoke.yml b/.github/workflows/fuzz-smoke.yml new file mode 100644 index 0000000..34e0ddc --- /dev/null +++ b/.github/workflows/fuzz-smoke.yml @@ -0,0 +1,25 @@ +name: fuzz-smoke + +on: + push: + branches: [master, main] + pull_request: + branches: [master, main] + +jobs: + libfuzzer: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + - name: Install cargo-fuzz + uses: taiki-e/install-action@cargo-fuzz + - name: Build fuzz targets (no sanitizer — stable) + run: cargo fuzz build -s none + working-directory: ${{ github.workspace }} + - name: Smoke all libFuzzer targets + run: bash scripts/fuzz-libfuzzer.sh + working-directory: ${{ github.workspace }} + env: + FUZZ_RUNS: "2000" diff --git a/cli/zkac_node.egg-info/SOURCES.txt b/cli/zkac_node.egg-info/SOURCES.txt index 75468e6..9e29da1 100644 --- a/cli/zkac_node.egg-info/SOURCES.txt +++ b/cli/zkac_node.egg-info/SOURCES.txt @@ -2,9 +2,11 @@ README.md pyproject.toml zkac_cli/__init__.py zkac_cli/client.py +zkac_cli/i2p_serve.py zkac_cli/main.py zkac_cli/paths.py zkac_cli/server.py +zkac_cli/server_debug.py zkac_cli/store.py zkac_node.egg-info/PKG-INFO zkac_node.egg-info/SOURCES.txt diff --git a/cli/zkac_node.egg-info/entry_points.txt b/cli/zkac_node.egg-info/entry_points.txt index e0a11c4..9029a1b 100644 --- a/cli/zkac_node.egg-info/entry_points.txt +++ b/cli/zkac_node.egg-info/entry_points.txt @@ -1,2 +1,3 @@ [console_scripts] zkac-node = zkac_cli.main:main +zkac-node-i2p-server = zkac_cli.i2p_serve:main diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..c46cc51 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,41 @@ +# ZKAC File-Share Demo + +This folder contains only the self-contained Textual file-share demo. + +## Files + +- `demo/file_share_server.py`: headless opaque server (registry mgmt + file-share channel). +- `demo/file_share_client.py`: upload/download + role-mask utilities. +- `demo/file_share_credentials.py`: P2P credential grant helper. +- `demo/file_share_tui.py`: Textual UI. +- `demo/zkac_cli_adapter.py`: subprocess bridge to `zkac-node`. +- `demo/file_share_smoke.py`: end-to-end smoke test. + +## Run + +```bash +uv sync --extra demo +uv run python demo/file_share_server.py --port 9879 +uv run python demo/file_share_tui.py +``` + +The demo uses `ZKAC_HOME=~/.ZKAC-FS` by default, so it stays isolated from other +local ZKAC usage. + +## UI Flow + +- `Login` +- `Connect` (reuses pinned server key when available) +- `Select Bucket` (list owned + permitted buckets, or create new) +- `Permissions` (edit per-role bitmask) +- `Share Permissions` +- `Listen` (optional port; blank means random) +- `Inbox` + +`c` copies the latest generated contact bundle to clipboard. + +## Verify + +```bash +uv run python demo/file_share_smoke.py +``` diff --git a/demo/__pycache__/cli_web_server.cpython-314.pyc b/demo/__pycache__/cli_web_server.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..46cba9e75c43160bfa869bec64b1b481a93571a9 GIT binary patch literal 16822 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 literal 0 HcmV?d00001 diff --git a/demo/__pycache__/file_share_client.cpython-314.pyc b/demo/__pycache__/file_share_client.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71dc4c1ff95493a16fc4119b4f4b53d02a820386 GIT binary patch literal 36061 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 WZXE>NyEX`2cdLl$+_P!)>Hi0Dzt)-n literal 0 HcmV?d00001 diff --git a/demo/__pycache__/file_share_credentials.cpython-314.pyc b/demo/__pycache__/file_share_credentials.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d871b0de873e11a1d34cee8d738aefe8e69efff GIT binary patch literal 19426 zcmb_^TW}jml30UyKZqAWka&=6iV`0ZA9_%tL`l?x)LSGmkf!O~;Ru9?CW#RV;2NMb z;?C@Fd}p6DYlo=0b%=AT4PkC|F`SFNI7N6rv=?^7+}RGV!#|*!JmC81+X=724u^j@ zCr!-8`aYaljp_y=NSfWdVz9cpsyeGGD=RB2GYh-y)fNWA&6R(Q|A&1H^DBIzK`SRT z&;A|~i%giwFjttc=A7niRi>&5(^coR87+D0GCK0qXY}N0$Qa?%9x6E%^5S~ z>CRcsS~J$Owv4TqU!Ads^>s|xP{+(VGmfwk(k7C2hRu++khCjog|v;N-Qj9T+ex}6 z?0~eBq&;C**xjQE*FcU3b7reDwc%QL^OACJxGr1||neo3>0$oJ*(YqlroW0)G>$b@Pcy z;btlWU$>vVFm#%YC#I4BJQ|yeC%Oja)4VV-(ZSw`XJ!E4Y&@Oe6Ge~~0ut($gN*SN zK_aZK0Rk96cC(jfl4+iujb7!UWAOw4&cu@m=;lo}7N44eR^kchJI|(sWF|S8oMr## z;tw$c%K7x95WmWkVuag6z`}O|KZ#H*$thA6FOHT?#Z&xjJi!wf=_Dx>lJkjJS4N1Z zXiMqn9Ipal=jPKH7Qh4AifD>VC51?F2<)y`jlGk-F%zGhVXr0ub64XqE6{TQv;_AQk9QJTXmP=c1X(86XCnNmd?}rPx!F zdvi7!jRCeMflAUq9s;5V@C)Mx^p=uPYHzAbp52KFoEjdcT;MI(5g?u-HYX;aT7+~7 z=w*&ir=!z63q7AtOwL3T(|oLxO(p<7&RVe_Fd2mQV31>gTbP8ayb#UsP%C;p8IQ5? zxl~ff;D{zBCTHV4NY)Wd?3|c5%*M%5$lUB?q(7-TvlMB~#l z8CJp7;Kg%oX~>UW1*9euU9ouj8hifI=m;y1D$QPxM`1>2gD|hr6hMWc$ty11ZRyZR z)@UM;%tVo@(j)M$3q>2Hd{sS(MeXDU9+N1zJ=A5mFJx;u4KvPGa%WH5Xr zA~@ii7Why5AzEaeiI+dHRSCXlrm%-n7{!2mB(=r5{t|@=8+J>9+!n? z7-w%010j7`A-sl_QW&#F(nr&i@py+;GF*+O`2)R@`RajQASEDR$siMQhej~LhiL@b zD%c^GbP=c}cp*s!)Ch@1X2a^rTLVw5f#o65+MKtxuUgwz>O||FoNkZcgew1($q|g$ zt|l#5;VIyDCYUj@K?EJFh6m~3Hqt?YYZA3#fr{`ZL~+xtNkn+l5>MXLzCm3Q6ohxf z>u-Z_MCnLJl~4oU5lku=tcRRZ3>qTy3B+Ll@<|`@lTsX>5Uq_lU8CB$-;t_i#2J8R zU*`oCdkc=td}@||3kbUtxN+v~*J5%Pj(}oPholS9a?2(@vOVNY?JU$K0SOSWsd@Gh zByb6a=rH7R600P61L_Lyl$NVPoM4!C;bI-f86hF8;h6p^8%QO4wUo?fG?z7pHOd!I zH^?-BTr#eZKc~H7=`AjneYn_llBR#b5l>u?&H_u6k;5){I$8eR6tHYy=CE02`jJyE znD^}M>)s82dtjMZgj5u`Cvqpr!bNB2`BQ?B6eKO+utO&_Kqn;~ehWN$_KVwTnZyuK&_%->}y& zA6q%_`OaML*_`j3Xg{AboiEs&d0SJ?*0gltVM}&uu326s9AJ>~ z$;@l1WiJdxp%$%X*H;WgS)dkptZXN5FH(yUJ|fie4+IgFsihanFr+l&3i}Hoe#-(d zLQR+%N0=mV8cHu6%oSayf#x$zr$Q%?RGI*>rS@N4_ib6iGYFCnQ6WRqNhK z13~Ih{74J6&{l_5XorV@d`Hp>(Hm)8LO_Bt1=bcRa59<5L?<(m;w;1AfFUy%h6GTe z$$rOn+x7$d;)xB5GjG}U#Ih}WVnzEi?V}U_-mr4xvFp#jx88nuef!~j%iwCupxAOs zw47cX+^{$vRA-!W0h|Rg*6t)E^*b#d&29sgM&~r-Avm0hB0Y;UtyHpw~3EOTJm_TC7E(x6$*n-aOZ)( z>YrUF-@k4f9@9{cld3A=XTb1*pD7dv{7fVBvm#&9atv@c9jAdEuV@AUv-!d!= zT!e#5MwP@tA8;sh%W4^RBy}wU&}1@gEt07>BJk~+d2i+vgPM*)N6(7sk>*kBqx#3e zO|7Q-jOLkE>wr?N*|KS3Y`&bX{zWRq6;7xWxx*_2NkXMzeb^8-hD|+sm@hM-R(Z5U zav>XpL*S?3WGCE=#A7&U$wGu}B0erS5bNE8%Au4H_Ck%kT@$gilO>PWZ`p&;-E=?c z^EXVRruHk03O2{47M@?iQxDHg1LO5=8Zl*JTa)4D9Hrv25b~8} zz$>Sx@aZ#UE@`D?)b!gB(oR`;HJ&+T3`!f8@g@pl>^5iV1+vDFvW_EHBw_3a3gfbI zwonVrSD)kCX*z_mOIc_J#uYa7C>va;qpSkFmT#j?B_bYY4WTqt7RtX!RsW8CgQXcZ zb5&sr{9DJnXfvva$!SA9G*^9c)f9*2&mINK-*TiX_I9iAX$q*{7Q%hFEW9v&J7*6e z7cC268|@zzh+}ZHfGwwwR^;qqTcuSsqZvO`Qm497oh>!&m1^v(g7tKm!cm{J|4ndY zWSPsasS{@oYa5ujro*u?a$+9T8=$MgZ7;^oP0wYzQ2UYKXJLbz<}<}sI)q|C6BUC!X^a=c2FVk@BVMOqwMj|HIgwCpW~DBnGvZfnUZw)J#27u zG09fim}I5R0uz^UA`d-Hn`XB_k?VpnC#tc2X_bV5x?t*!#4AOHeYTlatr@jO2n5A$$<*60&MevZVP; zaY&L5Hy24ag2#K~1z9C2Ij|Lq4BWBJq4}$`@kuC`EToL|RzOZJ0MmyP4r)3Dq+UrM zo1aUi1za(bgP;~Pg(=j_q-ThRKr*Ia6Nd>D_K;6OC3aoX0xBh2cpj#8PCh}Aldz

k&q(i|6`UO~!bf~12%!*tg8Pi3jkztDHkOY+7 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 literal 0 HcmV?d00001 diff --git a/demo/__pycache__/file_share_smoke.cpython-314.pyc b/demo/__pycache__/file_share_smoke.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66370b43685b49b2a02f672336b18ce6adf377df GIT binary patch literal 15721 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

$oB_`HE3V`!xB4mv74n@tUpUz`?Vag<&3D;a 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 KPG8RY|NjEl@l87b literal 0 HcmV?d00001 diff --git a/demo/__pycache__/simple_cli_client.cpython-314.pyc b/demo/__pycache__/simple_cli_client.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..934d6f01d63df0a96b1dd4dea5846bb4b8c0874f GIT binary patch literal 8230 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 btRz{63(c1AxVL=4`QJMAz|C-5shIx&=>k`O literal 0 HcmV?d00001 diff --git a/demo/__pycache__/simple_i2p_server.cpython-314.pyc b/demo/__pycache__/simple_i2p_server.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b1c90d0fc3606e4310bf549eb8ed76c3bd41cbb GIT binary patch literal 6746 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 LTbU2H&=CJ$VZ;N9 literal 0 HcmV?d00001 diff --git a/demo/zkac_admin_serve.py b/demo/__pycache__/zkac_admin_serve.cpython-314.pyc similarity index 52% rename from demo/zkac_admin_serve.py rename to demo/__pycache__/zkac_admin_serve.cpython-314.pyc index 6a232a5ae0402cf42827f0cb485356b121d2284b..bb4836d3bf6193ec093976c4097820daf7660c20 100644 GIT binary patch literal 13634 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 diff --git a/demo/__pycache__/zkac_cli_adapter.cpython-314.pyc b/demo/__pycache__/zkac_cli_adapter.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a697d9706ddf30d39b49f24794ac22b61ee1a7ec GIT binary patch literal 27584 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

!7zwuf+t+ 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 literal 0 HcmV?d00001 diff --git a/demo/creds/.gitignore b/demo/creds/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/demo/creds/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/demo/file_share_client.py b/demo/file_share_client.py new file mode 100644 index 0000000..14224cb --- /dev/null +++ b/demo/file_share_client.py @@ -0,0 +1,551 @@ +""" +ZKAC file-share client library (demo). + +Reusable, UI-agnostic helpers used by ``file_share_tui.py`` and the smoke test +harness. The library performs: + +* deterministic folder flattening and per-file content-key generation, +* per-role visibility bitmasks over the flattened index, +* opaque blob and per-recipient role-grant uploads to ``file_share_server.py``, +* role-credential authenticated download + decrypt against the same server. + +Authenticated sessions piggy-back on the ZKAC TCP framing/handshake from +``zkac.tcp``; the ``zkac-node`` CLI is only used (separately) for identity, +registry and direct P2P credential grants. +""" + +from __future__ import annotations + +import base64 +import hashlib +import json +import os +import socket +import uuid +from dataclasses import dataclass, field +from pathlib import Path +from typing import Iterable, Iterator + +import zkac +from zkac.tcp import FramedSession, client_handshake_anon + +CHUNK_SIZE = 128 * 1024 # plaintext bytes per stored blob (well under MAX_TCP_FRAME_BYTES) +DEFAULT_CONNECT_TIMEOUT_S = 8.0 + + +# ── small helpers ──────────────────────────────────────────────────── + +def _b64(data: bytes) -> str: + return base64.b64encode(data).decode("ascii") + + +def _unb64(s: str) -> bytes: + return base64.b64decode(s) + + +def _parse_server(server: str) -> tuple[str, int]: + host, sep, port_s = server.rpartition(":") + if not sep: + raise ValueError(f"invalid server {server!r}, expected host:port") + port = int(port_s, 10) + if not 1 <= port <= 65535: + raise ValueError(f"port out of range: {port}") + return (host or "127.0.0.1"), port + + +# ── data classes ───────────────────────────────────────────────────── + +@dataclass +class FileChunk: + blob_id: str + eph_pk_b64: str # ephemeral pubkey used by the encrypt step (HPKE-style) + + +@dataclass +class FileEntry: + rel_path: str + size: int + sha256_b64: str + content_secret_b64: str # 32-byte X25519 secret -> derives the file's recipient kp + chunks: list[FileChunk] = field(default_factory=list) + + def to_grant_dict(self) -> dict: + return { + "rel_path": self.rel_path, + "size": self.size, + "sha256_b64": self.sha256_b64, + "content_secret_b64": self.content_secret_b64, + "chunks": [{"blob_id": c.blob_id, "eph_pk_b64": c.eph_pk_b64} for c in self.chunks], + } + + @classmethod + def from_grant_dict(cls, data: dict) -> "FileEntry": + return cls( + rel_path=data["rel_path"], + size=int(data["size"]), + sha256_b64=data["sha256_b64"], + content_secret_b64=data["content_secret_b64"], + chunks=[FileChunk(c["blob_id"], c["eph_pk_b64"]) for c in data["chunks"]], + ) + + +@dataclass +class BucketManifest: + """Admin-side bucket manifest, persisted locally on the bucket creator.""" + + bucket_id: str + server: str + registry_id_hex: str + files: list[FileEntry] + role_masks: dict[str, str] = field(default_factory=dict) # role -> bitmask string ('0'/'1') + recipients: list[dict] = field(default_factory=list) # [{role_name, issuance_pk_hex}] + + def to_dict(self) -> dict: + return { + "bucket_id": self.bucket_id, + "server": self.server, + "registry_id_hex": self.registry_id_hex, + "files": [ + { + "rel_path": f.rel_path, + "size": f.size, + "sha256_b64": f.sha256_b64, + "content_secret_b64": f.content_secret_b64, + "chunks": [{"blob_id": c.blob_id, "eph_pk_b64": c.eph_pk_b64} for c in f.chunks], + } + for f in self.files + ], + "role_masks": self.role_masks, + "recipients": self.recipients, + } + + @classmethod + def from_dict(cls, data: dict) -> "BucketManifest": + files = [ + FileEntry( + rel_path=fd["rel_path"], + size=int(fd["size"]), + sha256_b64=fd["sha256_b64"], + content_secret_b64=fd["content_secret_b64"], + chunks=[FileChunk(c["blob_id"], c["eph_pk_b64"]) for c in fd["chunks"]], + ) + for fd in data.get("files", []) + ] + return cls( + bucket_id=data["bucket_id"], + server=data["server"], + registry_id_hex=data["registry_id_hex"], + files=files, + role_masks=dict(data.get("role_masks", {})), + recipients=list(data.get("recipients", [])), + ) + + +# ── folder flattening + chunking ───────────────────────────────────── + +def flatten_folder(folder: Path) -> list[Path]: + """Deterministic relative-path order: sorted, files only, skipping hidden entries.""" + folder = folder.resolve() + if not folder.is_dir(): + raise NotADirectoryError(folder) + out: list[Path] = [] + for path in sorted(folder.rglob("*")): + if not path.is_file(): + continue + if any(part.startswith(".") for part in path.relative_to(folder).parts): + continue + out.append(path) + return sorted(out, key=lambda p: str(p.relative_to(folder))) + + +def _read_chunks(path: Path, chunk_size: int = CHUNK_SIZE) -> Iterator[bytes]: + with path.open("rb") as fh: + while True: + buf = fh.read(chunk_size) + if not buf: + return + yield buf + + +def _sha256_file(path: Path) -> bytes: + h = hashlib.sha256() + for buf in _read_chunks(path, 64 * 1024): + h.update(buf) + return h.digest() + + +def encrypt_file_to_blobs(path: Path) -> tuple[FileEntry, list[bytes]]: + """Generate a per-file content keypair, encrypt each chunk to its public key. + + Each chunk uses ``zkac.encrypt_for_admin`` so the matching + ``IssuanceKeypair.decrypt`` on the recipient side succeeds (both bind the + ``user->admin`` HKDF label). + + Returns the file entry plus the parallel list of ciphertext blobs. + """ + content_kp = zkac.IssuanceKeypair() + content_pk = bytes(content_kp.public_key_bytes()) + content_sk = bytes(content_kp.secret_bytes()) + sha = _sha256_file(path) + size = path.stat().st_size + entry = FileEntry( + rel_path="", + size=size, + sha256_b64=_b64(sha), + content_secret_b64=_b64(content_sk), + ) + blobs: list[bytes] = [] + for chunk in _read_chunks(path): + eph_pk, ciphertext = zkac.encrypt_for_admin(content_pk, chunk) + entry.chunks.append(FileChunk(blob_id=uuid.uuid4().hex, eph_pk_b64=_b64(bytes(eph_pk)))) + blobs.append(bytes(ciphertext)) + return entry, blobs + + +def decrypt_file_from_blobs(entry: FileEntry, blobs: Iterable[bytes]) -> bytes: + """Reassemble + verify a file from its (already fetched) ciphertext blobs.""" + content_kp = zkac.IssuanceKeypair.from_secret(_unb64(entry.content_secret_b64)) + out = bytearray() + for chunk_meta, ciphertext in zip(entry.chunks, blobs): + out.extend(bytes(content_kp.decrypt(_unb64(chunk_meta.eph_pk_b64), ciphertext))) + if hashlib.sha256(bytes(out)).digest() != _unb64(entry.sha256_b64): + raise RuntimeError(f"hash mismatch for {entry.rel_path!r}") + return bytes(out) + + +# ── role bitmasks ──────────────────────────────────────────────────── + +def normalize_mask(mask: str, file_count: int) -> str: + """Pad/truncate a bitmask string of '0'/'1' to ``file_count`` and validate.""" + cleaned = "".join(c for c in mask if c in "01") + if len(cleaned) > file_count: + raise ValueError(f"mask {mask!r} longer than {file_count} files") + cleaned = cleaned.ljust(file_count, "0") + return cleaned + + +def files_visible_to_mask(files: list[FileEntry], mask: str) -> list[FileEntry]: + return [f for f, bit in zip(files, mask) if bit == "1"] + + +def encode_grant_payload( + bucket_id: str, + role_name: str, + server: str, + registry_id_hex: str, + visible_files: list[FileEntry], +) -> bytes: + payload = { + "bucket_id": bucket_id, + "role_name": role_name, + "server": server, + "registry_id_hex": registry_id_hex, + "files": [f.to_grant_dict() for f in visible_files], + } + return json.dumps(payload, separators=(",", ":")).encode("utf-8") + + +def encrypt_grant_to_recipient(payload: bytes, recipient_issuance_pk_hex: str) -> tuple[str, str]: + """User->admin direction sealed box: ``zkac.encrypt_for_admin`` -> (eph_pk_b64, ciphertext_b64).""" + rec_pk = bytes.fromhex(recipient_issuance_pk_hex) + if len(rec_pk) != 32: + raise ValueError("recipient issuance public key must be 32 bytes") + eph_pk, ciphertext = zkac.encrypt_for_admin(rec_pk, payload) + return _b64(bytes(eph_pk)), _b64(bytes(ciphertext)) + + +def decrypt_grant_for_recipient( + eph_pk_b64: str, + ciphertext_b64: str, + issuance_secret_hex: str, +) -> dict: + secret = bytes.fromhex(issuance_secret_hex) + if len(secret) != 32: + raise ValueError("issuance secret must be 32 bytes") + receiver = zkac.IssuanceKeypair.from_secret(secret) + plaintext = bytes(receiver.decrypt(_unb64(eph_pk_b64), _unb64(ciphertext_b64))) + return json.loads(plaintext.decode("utf-8")) + + +# ── encrypted authenticated session to file-share server ───────────── + +class FileShareSession: + """Anonymous handshake + ``op:'fs'`` BBS+ presentation -> framed JSON RPC.""" + + def __init__(self, sock: socket.socket, framed: FramedSession) -> None: + self._sock = sock + self._framed = framed + + def _call(self, cmd: dict) -> dict: + self._framed.send(json.dumps(cmd).encode("utf-8")) + resp = json.loads(self._framed.recv()) + if resp.get("error"): + raise RuntimeError(resp["error"]) + return resp + + def close(self) -> None: + try: + self._sock.close() + except OSError: + pass + + def __enter__(self) -> "FileShareSession": + return self + + def __exit__(self, *exc: object) -> None: + self.close() + + # admin commands + + def bucket_create(self, bucket_id: str | None = None) -> str: + cmd = {"cmd": "bucket_create"} + if bucket_id is not None: + cmd["bucket_id"] = bucket_id + return self._call(cmd)["bucket_id"] + + def bucket_put_blob(self, bucket_id: str, blob_id: str, ciphertext: bytes) -> None: + self._call({ + "cmd": "bucket_put_blob", + "bucket_id": bucket_id, + "blob_id": blob_id, + "ciphertext_b64": _b64(ciphertext), + }) + + def bucket_put_role_grant( + self, + bucket_id: str, + recipient_pk_hex: str, + role_id_hex: str, + acl_version: int, + eph_pk_b64: str, + ciphertext_b64: str, + ) -> None: + self._call({ + "cmd": "bucket_put_role_grant", + "bucket_id": bucket_id, + "recipient_pk_hex": recipient_pk_hex, + "role_id_hex": role_id_hex, + "acl_version": int(acl_version), + "eph_pk_b64": eph_pk_b64, + "ciphertext_b64": ciphertext_b64, + }) + + def bucket_set_role_acl(self, bucket_id: str, role_id_hex: str, allowed_blob_ids: list[str]) -> None: + self._call({ + "cmd": "bucket_set_role_acl", + "bucket_id": bucket_id, + "role_id_hex": role_id_hex, + "allowed_blob_ids": allowed_blob_ids, + }) + + def bucket_get_role_acl(self, bucket_id: str, role_id_hex: str) -> dict: + return self._call({ + "cmd": "bucket_get_role_acl", + "bucket_id": bucket_id, + "role_id_hex": role_id_hex, + }) + + def bucket_finalize(self, bucket_id: str) -> None: + self._call({"cmd": "bucket_finalize", "bucket_id": bucket_id}) + + def bucket_delete(self, bucket_id: str) -> None: + self._call({"cmd": "bucket_delete", "bucket_id": bucket_id}) + + def bucket_list_owned(self) -> list[str]: + return self._call({"cmd": "bucket_list_owned"})["bucket_ids"] + + # any-role commands + + def whoami(self) -> dict: + return self._call({"cmd": "whoami"}) + + def fs_buckets(self) -> list[str]: + return self._call({"cmd": "fs_buckets"})["bucket_ids"] + + def fs_get_role_grant(self, bucket_id: str) -> dict: + return self._call({"cmd": "fs_get_role_grant", "bucket_id": bucket_id}) + + def fs_get_blob(self, bucket_id: str, blob_id: str) -> bytes: + return _unb64(self._call({"cmd": "fs_get_blob", "bucket_id": bucket_id, "blob_id": blob_id})["ciphertext_b64"]) + + +def open_session( + server: str, + *, + server_pk_hex: str, + user_transport_secret: bytes, + registry_id_hex: str, + role_id: bytes, + credential: "zkac.Credential", + user_issuance_pk_hex: str, + connect_timeout_s: float = DEFAULT_CONNECT_TIMEOUT_S, +) -> FileShareSession: + """Connect, anonymous-handshake, then present a BBS+ proof bound to the transcript.""" + host, port = _parse_server(server) + sock = socket.create_connection((host, port), timeout=connect_timeout_s) + sock.settimeout(None) + try: + node = zkac.Node(zkac.Keypair.from_secret_key(user_transport_secret)) + server_pk = zkac.PublicKey.from_bytes(bytes.fromhex(server_pk_hex)) + session = client_handshake_anon(sock, node, server_pk) + framed = FramedSession(sock, session) + transcript_hash = bytes(session.transcript_hash()) + bbs_auth_proof = bytes(credential.present(transcript_hash)) + framed.send(json.dumps({ + "op": "fs", + "registry_id": registry_id_hex, + "role_id": role_id.hex(), + "bbs_auth_b64": _b64(bbs_auth_proof), + "issuance_pk_hex": user_issuance_pk_hex, + }).encode("utf-8")) + hello = json.loads(framed.recv()) + if hello.get("error"): + raise RuntimeError(hello["error"]) + return FileShareSession(sock, framed) + except Exception: + sock.close() + raise + + +# ── higher-level admin helpers ─────────────────────────────────────── + +def upload_bucket( + sess: FileShareSession, + folder: Path, + *, + server: str, + registry_id_hex: str, + bucket_id: str | None = None, +) -> BucketManifest: + """Encrypt every file under ``folder`` and stream blobs to the server.""" + bid = sess.bucket_create(bucket_id) + paths = flatten_folder(folder) + files: list[FileEntry] = [] + for p in paths: + entry, blobs = encrypt_file_to_blobs(p) + entry.rel_path = str(p.relative_to(folder.resolve())) + for chunk_meta, ciphertext in zip(entry.chunks, blobs): + sess.bucket_put_blob(bid, chunk_meta.blob_id, ciphertext) + files.append(entry) + sess.bucket_finalize(bid) + return BucketManifest( + bucket_id=bid, + server=server, + registry_id_hex=registry_id_hex, + files=files, + ) + + +def push_role_grant( + sess: FileShareSession, + manifest: BucketManifest, + role_name: str, + recipient_issuance_pk_hex: str, +) -> None: + """Encrypt the per-role visible-file subset to ``recipient`` and store on server.""" + if role_name not in manifest.role_masks: + raise RuntimeError(f"role {role_name!r} has no visibility mask in this bucket") + mask = normalize_mask(manifest.role_masks[role_name], len(manifest.files)) + visible = files_visible_to_mask(manifest.files, mask) + role_id_hex = zkac.role_id(role_name).hex() + acl_meta = sess.bucket_get_role_acl(manifest.bucket_id, role_id_hex) + acl_version = int(acl_meta.get("version", 0)) + if acl_version <= 0: + raise RuntimeError("server ACL missing for role; apply permissions before sharing") + payload = encode_grant_payload( + manifest.bucket_id, role_name, manifest.server, manifest.registry_id_hex, visible, + ) + eph_pk_b64, ct_b64 = encrypt_grant_to_recipient(payload, recipient_issuance_pk_hex) + sess.bucket_put_role_grant( + manifest.bucket_id, + recipient_issuance_pk_hex, + role_id_hex, + acl_version, + eph_pk_b64, + ct_b64, + ) + manifest.recipients.append({ + "role_name": role_name, + "issuance_pk_hex": recipient_issuance_pk_hex, + }) + + +def apply_role_masks_to_server(sess: FileShareSession, manifest: BucketManifest) -> None: + """Push per-role blob ACLs so server enforces mask on fs_get_blob.""" + if not manifest.role_masks: + return + for role_name, raw_mask in manifest.role_masks.items(): + mask = normalize_mask(raw_mask, len(manifest.files)) + visible = files_visible_to_mask(manifest.files, mask) + allowed_blob_ids = [c.blob_id for f in visible for c in f.chunks] + sess.bucket_set_role_acl( + manifest.bucket_id, + zkac.role_id(role_name).hex(), + allowed_blob_ids, + ) + + +def download_bucket( + sess: FileShareSession, + bucket_id: str, + *, + issuance_secret_hex: str, + output_dir: Path, +) -> dict: + """Fetch + decrypt every file the auth'd role has access to.""" + grant = sess.fs_get_role_grant(bucket_id) + payload = decrypt_grant_for_recipient(grant["eph_pk_b64"], grant["ciphertext_b64"], issuance_secret_hex) + if payload.get("bucket_id") != bucket_id: + raise RuntimeError("role grant bucket_id mismatch") + output_dir.mkdir(parents=True, exist_ok=True) + written: list[str] = [] + for fd in payload.get("files", []): + entry = FileEntry.from_grant_dict(fd) + ciphertexts = [sess.fs_get_blob(bucket_id, c.blob_id) for c in entry.chunks] + plaintext = decrypt_file_from_blobs(entry, ciphertexts) + out_path = output_dir / entry.rel_path + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_bytes(plaintext) + written.append(str(out_path)) + return {"role_name": payload.get("role_name"), "files_written": written} + + +# ── local persistence (admin manifests) ────────────────────────────── + +def state_dir(userid: str) -> Path: + # Keep demo state fully self-contained with demo ZKAC_HOME, rather than + # mixing with repository-local legacy state directories. + base_home = Path(os.environ.get("ZKAC_HOME", Path.home() / ".ZKAC-FS")) + base = base_home / userid / "file_share" + base.mkdir(parents=True, exist_ok=True) + try: + os.chmod(base, 0o700) + except OSError: + pass + return base + + +def manifest_path(userid: str, bucket_id: str) -> Path: + return state_dir(userid) / "buckets" / f"{bucket_id}.json" + + +def save_manifest(userid: str, manifest: BucketManifest) -> Path: + p = manifest_path(userid, manifest.bucket_id) + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(json.dumps(manifest.to_dict(), indent=2)) + try: + os.chmod(p, 0o600) + except OSError: + pass + return p + + +def load_manifest(userid: str, bucket_id: str) -> BucketManifest: + return BucketManifest.from_dict(json.loads(manifest_path(userid, bucket_id).read_text())) + + +def list_manifests(userid: str) -> list[str]: + base = state_dir(userid) / "buckets" + if not base.is_dir(): + return [] + return sorted(p.stem for p in base.glob("*.json")) diff --git a/demo/file_share_credentials.py b/demo/file_share_credentials.py new file mode 100644 index 0000000..5ec959d --- /dev/null +++ b/demo/file_share_credentials.py @@ -0,0 +1,321 @@ +""" +Demo-local P2P credential grant helper. + +The shipped ``zkac-node grant`` command pairs a sender that calls +``IssuanceKeypair.encrypt`` (HKDF info ``admin->user``) with a listener that +calls ``IssuanceKeypair.decrypt`` (HKDF info ``user->admin``). Those labels +intentionally differ in the protocol — they describe the user/admin direction +of the issuance pipeline — so the round-trip in the same direction must use +``encrypt_for_admin`` + ``IssuanceKeypair.decrypt``, which both bind the +``user->admin`` label. + +This module performs the grant from the demo using the matched pair so the +encrypted payload decodes correctly under ``zkac-node p2p-listen``. The wire +format of the ``p2p_grant`` message is unchanged, only the encryption call. + +We deliberately avoid importing ``cli/zkac_cli/*``; identity, admin and pin +material is read straight from the CLI's stable on-disk JSON formats via +``zkac_cli_adapter`` helpers. +""" + +from __future__ import annotations + +import base64 +import json +import socket +import time +from pathlib import Path + +import zkac +from zkac.tcp import FramedSession, client_handshake_anon + +import zkac_cli_adapter as cli + + +def _b64(data: bytes) -> str: + return base64.b64encode(data).decode("ascii") + + +def _unb64(s: str) -> bytes: + return base64.b64decode(s) + + +def _parse_server(server: str) -> tuple[str, int]: + host, sep, port_s = server.rpartition(":") + if not sep: + raise ValueError(f"invalid server {server!r}, expected host:port") + return (host or "127.0.0.1"), int(port_s, 10) + + +def _parse_contact_bundle(bundle: str) -> dict: + s = bundle.strip() + raw = base64.urlsafe_b64decode((s + "=" * (-len(s) % 4)).encode()) + return json.loads(raw.decode("utf-8")) + + +def _resolve_server_pk_hex(userid: str, server: str) -> str: + """Locate the user's pinned transport key for ``server`` on disk.""" + server_pk_hex = cli.load_pinned_server_key(userid, server) + if not server_pk_hex: + raise FileNotFoundError( + f"no pinned server key for {server!r} under {userid!r}; " + f"run: zkac-node server pin {userid} {server} --key " + ) + return server_pk_hex + + +def _fetch_registry_state( + userid: str, + server: str, + registry_id_hex: str, + admin_cred: zkac.Credential, +) -> tuple[bytes, bytes]: + """Authenticated mgmt-channel ``get_registry`` (matches the CLI's mgmt protocol).""" + server_pk_hex = _resolve_server_pk_hex(userid, server) + transport_secret = bytes.fromhex(cli.load_identity_secrets(userid)["transport_secret_hex"]) + host, port = _parse_server(server) + sock = socket.create_connection((host, port), timeout=8.0) + sock.settimeout(None) + try: + node = zkac.Node(zkac.Keypair.from_secret_key(transport_secret)) + server_pk = zkac.PublicKey.from_bytes(bytes.fromhex(server_pk_hex)) + session = client_handshake_anon(sock, node, server_pk) + framed = FramedSession(sock, session) + framed.send(json.dumps({"op": "mgmt"}).encode()) + transcript_hash = bytes(session.transcript_hash()) + framed.send(json.dumps({ + "cmd": "get_registry", + "registry_id": registry_id_hex, + "auth_registry_id": registry_id_hex, + "admin_proof_b64": _b64(bytes(admin_cred.present(transcript_hash))), + }).encode()) + resp = json.loads(framed.recv()) + if resp.get("error"): + raise RuntimeError(resp["error"]) + return _unb64(resp["state_bytes_b64"]), _unb64(resp["state_cert_b64"]) + finally: + sock.close() + + +# ── grant (sender side) ────────────────────────────────────────────── + +def grant_role_p2p( + admin_userid: str, + server: str, + registry_id_hex: str, + role_name: str, + recipient_contact_bundle: str, + *, + connect_timeout_s: float = 8.0, +) -> dict: + """Issue a BBS+ role credential and ship it to the recipient over an authenticated TCP session. + + Wire-compatible with ``zkac-node p2p-listen``: the ciphertext is produced via + ``encrypt_for_admin`` so the listener's ``IssuanceKeypair.decrypt`` succeeds. + """ + parsed = _parse_contact_bundle(recipient_contact_bundle) + recipient_issuance_pk_hex = parsed["issuance_pk_hex"] + recipient_transport_pk_hex = parsed["transport_pk_hex"] + recipient_grant_token_b64 = parsed["grant_token_b64"] + peer = parsed.get("peer") + if not peer: + raise RuntimeError( + "contact bundle missing peer endpoint; ask recipient to regenerate " + "with `zkac-node user show --peer `" + ) + + admin_data = cli.load_admin_material(admin_userid, registry_id_hex) + roles = admin_data.get("roles", []) + if role_name not in roles: + raise RuntimeError(f"role {role_name!r} not in registry (have: {roles})") + role_epochs = admin_data.get("role_epochs", {}) + if role_name not in role_epochs: + raise RuntimeError(f"missing epoch metadata for role {role_name!r}") + epoch = int(role_epochs[role_name]) + + bbs_issuer = zkac.BbsIssuer.from_secret_key(_unb64(admin_data["bbs_issuer_secret_b64"])) + bbs_pk = bbs_issuer.public_key() + admin_cred = cli.load_admin_credential(admin_userid, registry_id_hex) + + role_rid = zkac.role_id(role_name) + req = zkac.prepare_blind_request() + blind_sig = bbs_issuer.issue_blind(req.commitment_with_proof(), role_rid, epoch) + payload = json.dumps({ + "registry_id": registry_id_hex, + "role_name": role_name, + "epoch": epoch, + "issuer_pk_b64": _b64(bytes(bbs_pk.to_bytes())), + "blind_sig_b64": _b64(bytes(blind_sig)), + "member_secret_b64": _b64(bytes(req.member_secret())), + "prover_blind_b64": _b64(bytes(req.prover_blind())), + }).encode() + + rec_pk_bytes = bytes.fromhex(recipient_issuance_pk_hex) + if len(rec_pk_bytes) != 32: + raise RuntimeError("recipient issuance pubkey must decode to 32 bytes") + eph_pk, ciphertext = zkac.encrypt_for_admin(rec_pk_bytes, payload) + + state_bytes, state_cert = _fetch_registry_state( + admin_userid, server, registry_id_hex, admin_cred, + ) + + sender_transport_secret = bytes.fromhex( + cli.load_identity_secrets(admin_userid)["transport_secret_hex"] + ) + peer_host, peer_port = _parse_server(peer) + sock = socket.create_connection((peer_host, peer_port), timeout=connect_timeout_s) + sock.settimeout(None) + try: + node = zkac.Node(zkac.Keypair.from_secret_key(sender_transport_secret)) + peer_pk = zkac.PublicKey.from_bytes(bytes.fromhex(recipient_transport_pk_hex)) + session = client_handshake_anon(sock, node, peer_pk) + framed = FramedSession(sock, session) + ready = json.loads(framed.recv()) + if ready.get("error") or ready.get("op") != "ready_for_grant": + raise RuntimeError(f"peer did not accept grant session: {ready}") + transcript_hash = bytes(session.transcript_hash()) + admin_proof = bytes(admin_cred.present(transcript_hash)) + framed.send(json.dumps({ + "op": "p2p_grant", + "grant_token_b64": recipient_grant_token_b64, + "registry_id": registry_id_hex, + "registry_state_bytes_b64": _b64(state_bytes), + "registry_state_cert_b64": _b64(state_cert), + "role_name": role_name, + "eph_pk_b64": _b64(bytes(eph_pk)), + "ciphertext_b64": _b64(bytes(ciphertext)), + "admin_proof_b64": _b64(admin_proof), + }).encode()) + ack = json.loads(framed.recv()) + if ack.get("error"): + raise RuntimeError(ack["error"]) + return {"status": ack.get("status", "ok"), "peer": peer} + finally: + sock.close() + + +# ── listen (recipient side) ────────────────────────────────────────── + +def _save_credential_json( + userid: str, + registry_id_hex: str, + role_name: str, + payload: dict, +) -> Path: + """Write the received credential to ``ZKAC_HOME//credentials/_.json``. + + Mirrors the on-disk format used by ``zkac-node grant`` so subsequent + ``zkac-node`` commands can pick up the credential normally. + """ + creds_dir = cli.zkac_home() / userid / "credentials" + creds_dir.mkdir(parents=True, exist_ok=True) + target = creds_dir / f"{registry_id_hex}_{role_name}.json" + target.write_text(json.dumps({ + "blind_sig_b64": payload["blind_sig_b64"], + "member_secret_b64": payload["member_secret_b64"], + "prover_blind_b64": payload["prover_blind_b64"], + "role_name": payload["role_name"], + "epoch": payload["epoch"], + "issuer_pk_b64": payload["issuer_pk_b64"], + }, indent=2)) + try: + target.chmod(0o600) + except OSError: + pass + return target + + +def listen_for_role_credential( + userid: str, + host: str, + port: int, + *, + timeout_s: float = 60.0, +) -> dict: + """Block until one valid grant arrives, then save the credential locally. + + Compatible with ``demo.file_share_credentials.grant_role_p2p`` and (because + the wire format matches) also useful for testing against the existing + ``zkac-node grant`` listener API. + """ + from zkac.tcp import server_handshake_anon + + secrets = cli.load_identity_secrets(userid) + transport_secret = bytes.fromhex(secrets["transport_secret_hex"]) + issuance_secret = bytes.fromhex(secrets["issuance_secret_hex"]) + expected_token_b64 = secrets["grant_token_b64"] + + listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + listener.bind((host, port)) + listener.listen(1) + deadline = time.monotonic() + timeout_s + receiver_kp = zkac.IssuanceKeypair.from_secret(issuance_secret) + try: + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise RuntimeError("timed out waiting for authenticated grant sender") + listener.settimeout(remaining) + conn, _addr = listener.accept() + try: + conn.settimeout(min(remaining, 30.0)) + node = zkac.Node(zkac.Keypair.from_secret_key(transport_secret)) + session = server_handshake_anon(conn, node) + framed = FramedSession(conn, session) + framed.send(json.dumps({"ok": True, "op": "ready_for_grant"}).encode()) + msg = json.loads(framed.recv()) + if msg.get("error"): + raise RuntimeError(msg["error"]) + if msg.get("op") != "p2p_grant": + raise RuntimeError("unexpected p2p message") + if msg.get("grant_token_b64") != expected_token_b64: + raise RuntimeError("grant pairing token mismatch") + registry_id_hex = msg["registry_id"] + role_name = msg["role_name"] + + state_bytes = _unb64(msg["registry_state_bytes_b64"]) + state_cert = _unb64(msg["registry_state_cert_b64"]) + mgr = zkac.RegistryManager() + restored = mgr.restore(state_bytes, state_cert).hex() + if restored != registry_id_hex: + raise RuntimeError("registry snapshot does not match announced registry_id") + if not mgr.verify_admin( + bytes.fromhex(registry_id_hex), + _unb64(msg["admin_proof_b64"]), + bytes(session.transcript_hash()), + ): + raise RuntimeError("sender admin proof failed") + + payload = json.loads(receiver_kp.decrypt( + _unb64(msg["eph_pk_b64"]), _unb64(msg["ciphertext_b64"]), + )) + if payload.get("registry_id") != registry_id_hex: + raise RuntimeError("payload registry_id mismatch") + if payload.get("role_name") != role_name: + raise RuntimeError("payload role mismatch") + + cred = zkac.Credential.finalize( + _unb64(payload["blind_sig_b64"]), + _unb64(payload["member_secret_b64"]), + _unb64(payload["prover_blind_b64"]), + zkac.role_id(role_name), + int(payload["epoch"]), + zkac.BbsPublicKey.from_bytes(_unb64(payload["issuer_pk_b64"])), + ) + nonce = bytes(session.transcript_hash()) + if not mgr.verify_presentation( + bytes.fromhex(registry_id_hex), + zkac.role_id(role_name), + bytes(cred.present(nonce)), + nonce, + ): + raise RuntimeError("granted credential does not verify") + + _save_credential_json(userid, registry_id_hex, role_name, payload) + framed.send(json.dumps({"ok": True, "status": "stored"}).encode()) + return {"registry_id": registry_id_hex, "role": role_name} + finally: + conn.close() + finally: + listener.close() diff --git a/demo/file_share_server.py b/demo/file_share_server.py new file mode 100644 index 0000000..20d016c --- /dev/null +++ b/demo/file_share_server.py @@ -0,0 +1,653 @@ +#!/usr/bin/env python3 +""" +ZKAC opaque file-share server (demo). + +Headless TCP service that combines: + +* The same management-channel wire protocol as ``zkac-node serve`` so existing + ``zkac-node registry create/get/update`` commands work unchanged. +* A new file-share channel that, after BBS+ role authentication, exposes + bucket primitives. The server only ever sees opaque ciphertext blobs and + per-recipient encrypted role grants; file names, contents and per-role + visibility masks are never visible server-side. + +Run with:: + + uv run python demo/file_share_server.py --host 127.0.0.1 --port 9879 + +The server transport public key is printed at start-up; pin it on each client +with ``zkac-node server pin 127.0.0.1:9879 --key ``. +""" + +from __future__ import annotations + +import argparse +import base64 +import json +import os +import socket +import sys +import threading +import traceback +import uuid +from pathlib import Path +from typing import Any + +import zkac +from zkac.tcp import FramedSession, server_handshake_anon + + +# ── small helpers ──────────────────────────────────────────────────── + +def _b64(data: bytes) -> str: + return base64.b64encode(data).decode("ascii") + + +def _unb64(s: str) -> bytes: + return base64.b64decode(s) + + +def _chmod(path: Path, mode: int) -> None: + try: + os.chmod(path, mode) + except OSError: + pass + + +def _write_private_json(path: Path, payload: dict) -> None: + path.write_text(json.dumps(payload, indent=2)) + _chmod(path, 0o600) + + +def _is_loopback(host: str) -> bool: + return host.strip().lower() in {"127.0.0.1", "::1", "localhost"} + + +def _safe_id(value: str) -> str: + """Strict allowlist on identifiers used as filesystem names.""" + if not isinstance(value, str) or not value: + raise ValueError("missing identifier") + if len(value) > 128 or any(c not in "0123456789abcdefABCDEF-" for c in value): + raise ValueError(f"invalid identifier {value!r}") + return value + + +# ── opaque on-disk store ───────────────────────────────────────────── + +class _FileShareStore: + """Persists registry snapshots and opaque bucket state under one data dir.""" + + def __init__(self, data_dir: Path) -> None: + self._dir = data_dir + self._reg_dir = data_dir / "registries" + self._buckets_dir = data_dir / "buckets" + for d in (self._dir, self._reg_dir, self._buckets_dir): + d.mkdir(parents=True, exist_ok=True) + _chmod(d, 0o700) + self._lock = threading.Lock() + + # transport key + + def load_or_create_keypair(self) -> zkac.Keypair: + kf = self._dir / "server_key.json" + if kf.exists(): + data = json.loads(kf.read_text()) + return zkac.Keypair.from_secret_key(_unb64(data["secret_b64"])) + kp = zkac.Keypair() + _write_private_json(kf, { + "secret_b64": _b64(kp.secret_key_bytes()), + "public_b64": _b64(kp.public_key().to_bytes()), + }) + return kp + + # registries (mirrors zkac-node serve so the CLI works unchanged) + + def save_registry(self, rid_hex: str, state_bytes: bytes, cert_bytes: bytes) -> None: + with self._lock: + (self._reg_dir / f"{rid_hex}.state").write_bytes(state_bytes) + (self._reg_dir / f"{rid_hex}.cert").write_bytes(cert_bytes) + + def load_all_registries(self, mgr: zkac.RegistryManager) -> int: + n = 0 + for p in sorted(self._reg_dir.glob("*.state")): + cert = self._reg_dir / f"{p.stem}.cert" + if not cert.exists(): + continue + try: + mgr.restore(p.read_bytes(), cert.read_bytes()) + n += 1 + except Exception as exc: + print(f"[fs-server] skip registry {p.stem}: {exc}") + return n + + # buckets + + def _bucket_dir(self, bucket_id: str) -> Path: + return self._buckets_dir / _safe_id(bucket_id) + + def _bucket_meta_path(self, bucket_id: str) -> Path: + return self._bucket_dir(bucket_id) / "meta.json" + + def bucket_create(self, bucket_id: str, owner_registry_id_hex: str) -> None: + bd = self._bucket_dir(bucket_id) + with self._lock: + if bd.exists(): + raise RuntimeError("bucket already exists") + (bd / "blobs").mkdir(parents=True) + (bd / "role_grants").mkdir() + _chmod(bd, 0o700) + _write_private_json(self._bucket_meta_path(bucket_id), { + "bucket_id": bucket_id, + "owner_registry_id": owner_registry_id_hex, + "finalized": False, + "role_acl": {}, # role_id_hex -> {"version": int, "allowed_blob_ids": [blob_id...]} + }) + + def bucket_meta(self, bucket_id: str) -> dict: + return json.loads(self._bucket_meta_path(bucket_id).read_text()) + + def _bucket_require_owner(self, bucket_id: str, registry_id_hex: str) -> dict: + meta = self.bucket_meta(bucket_id) + if meta.get("owner_registry_id") != registry_id_hex: + raise RuntimeError("not bucket owner") + return meta + + def bucket_set_finalized(self, bucket_id: str, registry_id_hex: str, finalized: bool) -> None: + with self._lock: + meta = self._bucket_require_owner(bucket_id, registry_id_hex) + meta["finalized"] = bool(finalized) + _write_private_json(self._bucket_meta_path(bucket_id), meta) + + def bucket_delete(self, bucket_id: str, registry_id_hex: str) -> None: + with self._lock: + self._bucket_require_owner(bucket_id, registry_id_hex) + bd = self._bucket_dir(bucket_id) + for sub in ("blobs", "role_grants"): + d = bd / sub + if d.is_dir(): + for p in d.iterdir(): + try: + p.unlink() + except OSError: + pass + try: + d.rmdir() + except OSError: + pass + try: + self._bucket_meta_path(bucket_id).unlink() + except OSError: + pass + try: + bd.rmdir() + except OSError: + pass + + def bucket_put_blob( + self, + bucket_id: str, + registry_id_hex: str, + blob_id: str, + ciphertext: bytes, + ) -> None: + with self._lock: + self._bucket_require_owner(bucket_id, registry_id_hex) + (self._bucket_dir(bucket_id) / "blobs" / _safe_id(blob_id)).write_bytes(ciphertext) + + def bucket_put_role_grant( + self, + bucket_id: str, + registry_id_hex: str, + recipient_pk_hex: str, + role_id_hex: str, + acl_version: int, + eph_pk_b64: str, + ciphertext_b64: str, + ) -> None: + with self._lock: + self._bucket_require_owner(bucket_id, registry_id_hex) + _safe_id(recipient_pk_hex) + payload = { + "bucket_id": bucket_id, + "role_id_hex": _safe_id(role_id_hex), + "acl_version": int(acl_version), + "eph_pk_b64": eph_pk_b64, + "ciphertext_b64": ciphertext_b64, + } + target = self._bucket_dir(bucket_id) / "role_grants" / f"{recipient_pk_hex}.json" + _write_private_json(target, payload) + + def bucket_set_role_acl( + self, + bucket_id: str, + registry_id_hex: str, + role_id_hex: str, + allowed_blob_ids: list[str], + ) -> None: + with self._lock: + meta = self._bucket_require_owner(bucket_id, registry_id_hex) + role_acl = dict(meta.get("role_acl", {})) + key = _safe_id(role_id_hex) + prev = role_acl.get(key, {}) + prev_version = int(prev.get("version", 0)) if isinstance(prev, dict) else 0 + role_acl[key] = { + "version": prev_version + 1, + "allowed_blob_ids": [_safe_id(b) for b in allowed_blob_ids], + } + meta["role_acl"] = role_acl + _write_private_json(self._bucket_meta_path(bucket_id), meta) + + def bucket_get_blob(self, bucket_id: str, blob_id: str) -> bytes: + return (self._bucket_dir(bucket_id) / "blobs" / _safe_id(blob_id)).read_bytes() + + def bucket_blob_allowed_for_role(self, bucket_id: str, role_id_hex: str, blob_id: str) -> bool: + meta = self.bucket_meta(bucket_id) + role_acl = meta.get("role_acl", {}) + role_meta = role_acl.get(_safe_id(role_id_hex)) + if not isinstance(role_meta, dict): + return False + allowed = role_meta.get("allowed_blob_ids") + if not isinstance(allowed, list): + return False + return _safe_id(blob_id) in {str(v) for v in allowed} + + def bucket_role_acl(self, bucket_id: str, role_id_hex: str) -> dict: + meta = self.bucket_meta(bucket_id) + role_acl = meta.get("role_acl", {}) + role_meta = role_acl.get(_safe_id(role_id_hex), {}) + if not isinstance(role_meta, dict): + return {"version": 0, "allowed_blob_ids": []} + version = int(role_meta.get("version", 0)) + allowed = role_meta.get("allowed_blob_ids", []) + if not isinstance(allowed, list): + allowed = [] + return { + "version": version, + "allowed_blob_ids": [_safe_id(str(v)) for v in allowed], + } + + def bucket_get_role_grant(self, bucket_id: str, recipient_pk_hex: str) -> dict: + path = self._bucket_dir(bucket_id) / "role_grants" / f"{_safe_id(recipient_pk_hex)}.json" + return json.loads(path.read_text()) + + def buckets_for_recipient(self, recipient_pk_hex: str) -> list[str]: + """Bucket ids that have an encrypted role grant addressed to ``recipient_pk_hex``.""" + _safe_id(recipient_pk_hex) + out: list[str] = [] + for bd in sorted(self._buckets_dir.iterdir()): + grant = bd / "role_grants" / f"{recipient_pk_hex}.json" + if grant.is_file(): + out.append(bd.name) + return out + + def buckets_owned_by(self, registry_id_hex: str) -> list[str]: + out: list[str] = [] + for bd in sorted(self._buckets_dir.iterdir()): + meta = bd / "meta.json" + if not meta.is_file(): + continue + try: + if json.loads(meta.read_text()).get("owner_registry_id") == registry_id_hex: + out.append(bd.name) + except (OSError, json.JSONDecodeError): + continue + return out + + +# ── command dispatch (inside encrypted session) ────────────────────── + +def _dispatch_mgmt( + cmd: dict, + mgr: zkac.RegistryManager, + store: _FileShareStore, + server_pk_b64: str, + transcript_hash: bytes, +) -> dict: + """Registry management commands, wire-compatible with ``zkac-node`` CLI.""" + try: + action = cmd.get("cmd") + rid_hex = cmd.get("auth_registry_id") + admin_proof_b64 = cmd.get("admin_proof_b64") + + def _require_admin_for(target_rid_hex: str) -> None: + if rid_hex != target_rid_hex: + raise RuntimeError("auth_registry_id must match command registry_id") + if not isinstance(admin_proof_b64, str) or not admin_proof_b64: + raise RuntimeError("missing admin_proof_b64") + if not mgr.verify_admin( + bytes.fromhex(target_rid_hex), + _unb64(admin_proof_b64), + transcript_hash, + ): + raise RuntimeError("admin authorization failed") + + if action == "server_info": + return {"ok": True, "server_public_key_b64": server_pk_b64} + + if action == "create_registry": + state_bytes = _unb64(cmd["state_bytes_b64"]) + state_cert = _unb64(cmd["state_cert_b64"]) + auth_rid = cmd.get("auth_registry_id") + if not isinstance(auth_rid, str): + raise RuntimeError("missing auth_registry_id") + if not isinstance(admin_proof_b64, str) or not admin_proof_b64: + raise RuntimeError("missing admin_proof_b64") + tmp_mgr = zkac.RegistryManager() + expected_rid = tmp_mgr.create(state_bytes, state_cert).hex() + if expected_rid != auth_rid: + raise RuntimeError("auth_registry_id does not match certified state") + if not tmp_mgr.verify_admin( + bytes.fromhex(expected_rid), + _unb64(admin_proof_b64), + transcript_hash, + ): + raise RuntimeError("admin authorization failed for create_registry") + rid = mgr.create(state_bytes, state_cert) + store.save_registry(rid.hex(), state_bytes, state_cert) + return {"ok": True, "registry_id": rid.hex()} + + if action == "get_registry": + rid_hex_cmd = cmd["registry_id"] + _require_admin_for(rid_hex_cmd) + rid = bytes.fromhex(rid_hex_cmd) + state_bytes, state_cert = mgr.get(rid) + return { + "ok": True, + "state_bytes_b64": _b64(state_bytes), + "state_cert_b64": _b64(state_cert), + } + + if action == "update_registry": + rid_hex_cmd = cmd["registry_id"] + _require_admin_for(rid_hex_cmd) + rid = bytes.fromhex(rid_hex_cmd) + state_bytes = _unb64(cmd["state_bytes_b64"]) + state_cert = _unb64(cmd["state_cert_b64"]) + mgr.update(rid, state_bytes, state_cert) + store.save_registry(rid_hex_cmd, state_bytes, state_cert) + return {"ok": True} + + return {"error": f"unknown command: {action}"} + + except Exception as exc: + return {"error": str(exc)} + + +def _dispatch_fs( + cmd: dict, + store: _FileShareStore, + ctx: dict, +) -> dict: + """File-share commands authenticated via ``ctx`` (registry_id, role_id, issuance_pk_hex).""" + try: + action = cmd.get("cmd") + registry_id_hex: str = ctx["registry_id_hex"] + role_id: bytes = ctx["role_id"] + issuance_pk_hex: str = ctx["issuance_pk_hex"] + is_admin = role_id == zkac.admin_role_id() + + def _require_admin() -> None: + if not is_admin: + raise RuntimeError("admin role required for this command") + + if action == "whoami": + return { + "ok": True, + "registry_id": registry_id_hex, + "role_id": role_id.hex(), + "is_admin": is_admin, + "issuance_pk_hex": issuance_pk_hex, + } + + if action == "bucket_create": + _require_admin() + bid = cmd.get("bucket_id") or uuid.uuid4().hex + store.bucket_create(bid, registry_id_hex) + return {"ok": True, "bucket_id": bid} + + if action == "bucket_put_blob": + _require_admin() + bid = cmd["bucket_id"] + blob_id = cmd["blob_id"] + ciphertext = _unb64(cmd["ciphertext_b64"]) + store.bucket_put_blob(bid, registry_id_hex, blob_id, ciphertext) + return {"ok": True} + + if action == "bucket_put_role_grant": + _require_admin() + store.bucket_put_role_grant( + cmd["bucket_id"], + registry_id_hex, + cmd["recipient_pk_hex"], + cmd["role_id_hex"], + int(cmd["acl_version"]), + cmd["eph_pk_b64"], + cmd["ciphertext_b64"], + ) + return {"ok": True} + + if action == "bucket_set_role_acl": + _require_admin() + store.bucket_set_role_acl( + cmd["bucket_id"], + registry_id_hex, + cmd["role_id_hex"], + list(cmd.get("allowed_blob_ids", [])), + ) + return {"ok": True} + + if action == "bucket_get_role_acl": + _require_admin() + role_acl = store.bucket_role_acl(cmd["bucket_id"], cmd["role_id_hex"]) + return {"ok": True, **role_acl} + + if action == "bucket_finalize": + _require_admin() + store.bucket_set_finalized(cmd["bucket_id"], registry_id_hex, True) + return {"ok": True} + + if action == "bucket_delete": + _require_admin() + store.bucket_delete(cmd["bucket_id"], registry_id_hex) + return {"ok": True} + + if action == "bucket_list_owned": + _require_admin() + return {"ok": True, "bucket_ids": store.buckets_owned_by(registry_id_hex)} + + if action == "fs_buckets": + return {"ok": True, "bucket_ids": store.buckets_for_recipient(issuance_pk_hex)} + + if action == "fs_get_role_grant": + bid = cmd["bucket_id"] + grant = store.bucket_get_role_grant(bid, issuance_pk_hex) + if not is_admin: + expected_role = _safe_id(role_id.hex()) + granted_role_raw = grant.get("role_id_hex") + if not isinstance(granted_role_raw, str) or not granted_role_raw: + raise RuntimeError("permissions updated; role grant is outdated, request a fresh grant") + granted_role = _safe_id(granted_role_raw) + if granted_role != expected_role: + raise RuntimeError("role grant not valid for authenticated role") + current_acl = store.bucket_role_acl(bid, expected_role) + acl_version = grant.get("acl_version") + if not isinstance(acl_version, int): + raise RuntimeError("permissions updated; role grant is outdated, request a fresh grant") + if acl_version != int(current_acl.get("version", -2)): + raise RuntimeError("permissions updated; request a fresh role grant") + return {"ok": True, **grant} + + if action == "fs_get_blob": + bid = cmd["bucket_id"] + blob_id = cmd["blob_id"] + if not is_admin and not store.bucket_blob_allowed_for_role(bid, role_id.hex(), blob_id): + raise RuntimeError("blob access denied by role mask") + data = store.bucket_get_blob(bid, blob_id) + return {"ok": True, "ciphertext_b64": _b64(data)} + + return {"error": f"unknown command: {action}"} + + except Exception as exc: + return {"error": str(exc)} + + +# ── per-connection handler ──────────────────────────────────────────── + +def _handle_conn( + conn: socket.socket, + addr: tuple, + node: zkac.Node, + mgr: zkac.RegistryManager, + store: _FileShareStore, + server_pk_b64: str, + idle_timeout_s: float, + slots: threading.BoundedSemaphore, +) -> None: + peer = f"{addr[0]}:{addr[1]}" + try: + conn.settimeout(idle_timeout_s) + session = server_handshake_anon(conn, node) + framed = FramedSession(conn, session) + transcript_hash = bytes(session.transcript_hash()) + + hello = json.loads(framed.recv()) + op = hello.get("op") + + if op == "mgmt": + while True: + try: + cmd = json.loads(framed.recv()) + except (ConnectionError, OSError): + break + resp = _dispatch_mgmt(cmd, mgr, store, server_pk_b64, transcript_hash) + framed.send(json.dumps(resp).encode()) + return + + if op == "fs": + try: + registry_id = bytes.fromhex(hello["registry_id"]) + role_id = bytes.fromhex(hello["role_id"]) + proof = _unb64(hello["bbs_auth_b64"]) + issuance_pk_hex = hello["issuance_pk_hex"] + _safe_id(issuance_pk_hex) + except (KeyError, ValueError) as exc: + framed.send(json.dumps({"error": f"invalid fs hello: {exc}"}).encode()) + return + if role_id == zkac.admin_role_id(): + ok = mgr.verify_admin(registry_id, proof, transcript_hash) + else: + ok = mgr.verify_presentation(registry_id, role_id, proof, transcript_hash) + if not ok: + framed.send(json.dumps({"error": "auth failed"}).encode()) + return + framed.send(json.dumps({"ok": True, "status": "authenticated"}).encode()) + ctx = { + "registry_id_hex": registry_id.hex(), + "role_id": role_id, + "issuance_pk_hex": issuance_pk_hex, + } + while True: + try: + cmd = json.loads(framed.recv()) + except (ConnectionError, OSError): + break + resp = _dispatch_fs(cmd, store, ctx) + framed.send(json.dumps(resp).encode()) + return + + framed.send(json.dumps({"error": f"unknown op: {op}"}).encode()) + + except (ConnectionError, BrokenPipeError, OSError): + pass + except Exception as exc: + print(f"[fs-server] {peer} error: {exc}") + traceback.print_exc() + finally: + conn.close() + slots.release() + + +# ── entry point ────────────────────────────────────────────────────── + +def serve( + data_dir: Path, + host: str = "127.0.0.1", + port: int = 9879, + *, + max_connections: int = 64, + idle_timeout_s: float = 60.0, + listen_backlog: int = 64, + allow_non_loopback: bool = False, +) -> None: + data_dir.mkdir(parents=True, exist_ok=True) + store = _FileShareStore(data_dir) + kp = store.load_or_create_keypair() + server_pk_b64 = _b64(kp.public_key().to_bytes()) + pk_hex = kp.public_key().to_bytes().hex() + node = zkac.Node(kp) + + mgr = zkac.RegistryManager() + n = store.load_all_registries(mgr) + + print(f"[fs-server] data dir: {data_dir}") + print(f"[fs-server] server transport public key (pin OOB): {pk_hex}") + print(f"[fs-server] loaded {n} registries") + print(f"[fs-server] listening on {host}:{port}") + + if not _is_loopback(host) and not allow_non_loopback: + raise RuntimeError( + "refusing to bind outside loopback. " + "Use --allow-non-loopback only when exposure is intentional." + ) + if not _is_loopback(host): + print(f"[fs-server] warning: binding outside loopback: {host}:{port}", file=sys.stderr) + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind((host, port)) + slots = threading.BoundedSemaphore(max_connections) + sock.listen(listen_backlog) + try: + while True: + conn, addr = sock.accept() + if not slots.acquire(blocking=False): + conn.close() + continue + threading.Thread( + target=_handle_conn, + args=(conn, addr, node, mgr, store, server_pk_b64, idle_timeout_s, slots), + daemon=True, + ).start() + except KeyboardInterrupt: + print("\n[fs-server] shutdown") + finally: + sock.close() + + +def main() -> None: + p = argparse.ArgumentParser(description="ZKAC opaque file-share server (demo)") + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--port", type=int, default=9879) + p.add_argument( + "--data-dir", + type=Path, + default=Path(__file__).resolve().parent / "fs_data", + help="server state (transport key, registries, opaque buckets)", + ) + p.add_argument("--idle-timeout", type=float, default=60.0) + p.add_argument("--max-connections", type=int, default=64) + p.add_argument("--allow-non-loopback", action="store_true") + args = p.parse_args() + + serve( + args.data_dir, + host=args.host, + port=args.port, + max_connections=args.max_connections, + idle_timeout_s=args.idle_timeout, + allow_non_loopback=args.allow_non_loopback, + ) + + +if __name__ == "__main__": + main() diff --git a/demo/file_share_smoke.py b/demo/file_share_smoke.py new file mode 100644 index 0000000..1f4ba86 --- /dev/null +++ b/demo/file_share_smoke.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +""" +End-to-end smoke test for the ZKAC file-share demo. + +Exercises: + +* admin (Alice) creates a registry on the file-share server, +* admin uploads a folder as an encrypted bucket with per-role visibility masks, +* Alice issues a ZKAC role credential to Bob via direct P2P grant, +* Alice pushes a per-recipient bucket role grant to Bob, +* Bob authenticates to the file-share server with his role credential, + downloads, and decrypts the files his role can see, +* server opacity: every byte at rest in the bucket directory is ciphertext + (no plaintext file content survives on disk). + +All ZKAC operations go through ``zkac-node`` (subprocess) and a temporary +``ZKAC_HOME`` so the test is hermetic. + +Run:: + + uv run python demo/file_share_smoke.py +""" + +from __future__ import annotations + +import os +import shutil +import socket +import sys +import tempfile +import threading +import time +import traceback +from pathlib import Path + +DEMO_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(DEMO_DIR)) + +import zkac # noqa: E402 + +import file_share_client as fsc # noqa: E402 +import file_share_credentials as fscred # noqa: E402 +import file_share_server as fss # noqa: E402 +import zkac_cli_adapter as cli # noqa: E402 + + +def _free_port() -> int: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + finally: + s.close() + + +def _wait_until(predicate, timeout_s: float = 5.0, interval_s: float = 0.05) -> bool: + deadline = time.monotonic() + timeout_s + while time.monotonic() < deadline: + if predicate(): + return True + time.sleep(interval_s) + return False + + +def _make_files(folder: Path) -> dict[str, bytes]: + folder.mkdir(parents=True, exist_ok=True) + contents = { + "alpha.txt": b"public-summary: " + os.urandom(2048), + "nested/beta.bin": os.urandom(200 * 1024), # > one chunk + "secret.md": b"# top-secret\n" + os.urandom(8192), + } + paths: dict[str, bytes] = {} + for rel, data in contents.items(): + target = folder / rel + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + paths[rel] = data + return paths + + +def _start_fs_server(host: str, port: int, data_dir: Path) -> threading.Thread: + t = threading.Thread( + target=fss.serve, + args=(data_dir,), + kwargs={"host": host, "port": port, "allow_non_loopback": False}, + daemon=True, + ) + t.start() + return t + + +def _server_transport_pubkey_hex(data_dir: Path) -> str: + import json + return zkac.PublicKey.from_bytes( + __import__("base64").b64decode( + json.loads((data_dir / "server_key.json").read_text())["public_b64"] + ) + ).to_bytes().hex() + + +def _open_admin_session(userid: str, server: str, server_pk_hex: str, registry_id: str) -> fsc.FileShareSession: + secrets = cli.load_identity_secrets(userid) + cred = cli.load_admin_credential(userid, registry_id) + return fsc.open_session( + server, + server_pk_hex=server_pk_hex, + user_transport_secret=bytes.fromhex(secrets["transport_secret_hex"]), + registry_id_hex=registry_id, + role_id=zkac.admin_role_id(), + credential=cred, + user_issuance_pk_hex=secrets["issuance_pk_hex"], + ) + + +def _open_role_session(userid: str, server: str, server_pk_hex: str, registry_id: str, role_name: str) -> fsc.FileShareSession: + secrets = cli.load_identity_secrets(userid) + cred = cli.load_credential(userid, registry_id, role_name) + return fsc.open_session( + server, + server_pk_hex=server_pk_hex, + user_transport_secret=bytes.fromhex(secrets["transport_secret_hex"]), + registry_id_hex=registry_id, + role_id=zkac.role_id(role_name), + credential=cred, + user_issuance_pk_hex=secrets["issuance_pk_hex"], + ) + + +def _scan_for_plaintext(haystack_root: Path, needles: list[bytes]) -> list[tuple[Path, int]]: + """Brute-force grep for any ``needle`` bytes appearing in any file under ``haystack_root``.""" + hits: list[tuple[Path, int]] = [] + for p in haystack_root.rglob("*"): + if not p.is_file(): + continue + data = p.read_bytes() + for n in needles: + if len(n) >= 64 and n in data: + hits.append((p, len(n))) + break + return hits + + +def main() -> int: + tmp = Path(tempfile.mkdtemp(prefix="zkac-fs-smoke-")) + print(f"[smoke] temp dir: {tmp}") + home = tmp / "zkac_home" + fs_data = tmp / "fs_data" + src = tmp / "src_folder" + out = tmp / "downloads" + home.mkdir() + fs_data.mkdir() + + os.environ["ZKAC_HOME"] = str(home) + + contents = _make_files(src) + print(f"[smoke] source folder: {src} ({len(contents)} files)") + + port = _free_port() + server_addr = f"127.0.0.1:{port}" + _start_fs_server("127.0.0.1", port, fs_data) + if not _wait_until(lambda: (fs_data / "server_key.json").is_file()): + raise RuntimeError("server did not start") + if not _wait_until(lambda: socket.create_connection(("127.0.0.1", port), timeout=0.2)): + raise RuntimeError("server port not ready") + server_pk_hex = _server_transport_pubkey_hex(fs_data) + print(f"[smoke] file-share server ready @ {server_addr}, pk={server_pk_hex[:16]}…") + + cli.run_cli(["user", "create", "alice"]).raise_for_status() + cli.run_cli(["user", "create", "bob"]).raise_for_status() + cli.server_pin("alice", server_addr, server_pk_hex).raise_for_status() + cli.server_pin("bob", server_addr, server_pk_hex).raise_for_status() + print("[smoke] alice + bob created and pinned server") + + rid = cli.registry_create("alice", server_addr, ["viewer", "editor"]) + print(f"[smoke] registry created: {rid}") + + # --- direct P2P grant: bob listens, alice grants ---------------------- + listener_port = _free_port() + listener = cli.P2PListener("bob", host="127.0.0.1", port=listener_port, timeout_s=30.0) + listener.start() + if not _wait_until(lambda: listener.is_running()): + raise RuntimeError("bob listener did not start") + bob_contact = cli.show_user_contact("bob", peer=f"127.0.0.1:{listener_port}") + print(f"[smoke] bob listening on 127.0.0.1:{listener_port}") + try: + fscred.grant_role_p2p("alice", server_addr, rid, "viewer", bob_contact) + except RuntimeError as exc: + listener.stop() + print(f"[smoke] grant failed: {exc}\n[smoke] bob listener output:\n{listener.output()}") + raise + if not _wait_until(lambda: not listener.is_running(), timeout_s=10.0): + listener.stop() + raise RuntimeError(f"listener never exited after grant; output:\n{listener.output()}") + received = listener.parse_received() + print(f"[smoke] bob received: {received}") + assert received and received["role"] == "viewer" + assert received["registry_id"] == rid + + # --- alice uploads bucket + sets masks -------------------------------- + files_in_order = fsc.flatten_folder(src) + rel_paths = [str(p.relative_to(src.resolve())) for p in files_in_order] + print(f"[smoke] flattened: {rel_paths}") + # mask: viewer sees only first file (alpha.txt), editor sees everything. + viewer_mask = "1" + "0" * (len(files_in_order) - 1) + editor_mask = "1" * len(files_in_order) + + with _open_admin_session("alice", server_addr, server_pk_hex, rid) as sess: + manifest = fsc.upload_bucket( + sess, src, server=server_addr, registry_id_hex=rid, + ) + manifest.role_masks = {"viewer": viewer_mask, "editor": editor_mask} + # bob's issuance pk -> push viewer role grant + bob_secrets = cli.load_identity_secrets("bob") + fsc.push_role_grant(sess, manifest, "viewer", bob_secrets["issuance_pk_hex"]) + fsc.save_manifest("alice", manifest) + print(f"[smoke] uploaded bucket {manifest.bucket_id} with role grants") + + # --- bob downloads using his viewer credential ------------------------ + with _open_role_session("bob", server_addr, server_pk_hex, rid, "viewer") as sess: + accessible = sess.fs_buckets() + assert accessible == [manifest.bucket_id], accessible + result = fsc.download_bucket( + sess, manifest.bucket_id, + issuance_secret_hex=bob_secrets["issuance_secret_hex"], + output_dir=out / manifest.bucket_id, + ) + print(f"[smoke] bob downloaded files: {result['files_written']}") + + # --- assertions ------------------------------------------------------- + expected_visible = {rel_paths[0]} # only first file + actual = {str(Path(p).relative_to(out / manifest.bucket_id)) for p in result["files_written"]} + assert actual == expected_visible, f"visibility mismatch: {actual} vs {expected_visible}" + decrypted = (out / manifest.bucket_id / rel_paths[0]).read_bytes() + assert decrypted == contents[rel_paths[0]], "decrypted content does not match plaintext" + + # opacity: no plaintext file content lives on disk under fs_data + plaintext_needles = list(contents.values()) + leaks = _scan_for_plaintext(fs_data / "buckets", plaintext_needles) + assert not leaks, f"plaintext leaked into server storage: {leaks}" + + print("[smoke] OK: visibility enforced, content matches, server storage opaque") + print(f"[smoke] cleanup {tmp}") + shutil.rmtree(tmp, ignore_errors=True) + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception: + traceback.print_exc() + sys.exit(1) diff --git a/demo/file_share_tui.py b/demo/file_share_tui.py new file mode 100644 index 0000000..4db6be9 --- /dev/null +++ b/demo/file_share_tui.py @@ -0,0 +1,1003 @@ +#!/usr/bin/env python3 +""" +ZKAC file-share demo Textual TUI. + +Run the headless server in another terminal:: + + uv run python demo/file_share_server.py --port 9879 + +Then start the Textual app:: + + uv run python demo/file_share_tui.py +""" + +from __future__ import annotations + +import base64 +import json +import os +import shutil +import subprocess +import sys +import time +import traceback +from pathlib import Path + +import zkac +from textual import events +from textual.app import App, ComposeResult +from textual.containers import Horizontal, Vertical +from textual.screen import ModalScreen +from textual.widgets import Button, Footer, Header, Input, Label, Static, TextArea + +import file_share_client as fsc +import file_share_credentials as fscred +import zkac_cli_adapter as cli + + +class PromptScreen(ModalScreen[str | None]): + AUTO_FOCUS = "#prompt_input" + BINDINGS = [ + ("escape", "cancel", "Cancel"), + ("ctrl+q", "quit_app", "Quit"), + ("q", "quit_app", "Quit"), + ("ctrl+c", "quit_app", "Quit"), + ] + + def __init__(self, label: str, default: str = "") -> None: + super().__init__() + self._label = label + self._default = default + + def compose(self) -> ComposeResult: + with Vertical(id="prompt_dialog"): + yield Label(self._label, id="prompt_label") + yield Input(value=self._default, id="prompt_input") + with Horizontal(id="prompt_buttons"): + yield Button("OK", id="ok", variant="primary") + yield Button("Cancel", id="cancel") + + def on_mount(self) -> None: + # Defer focus until after layout settles so typing works immediately. + self.call_after_refresh(self._focus_prompt_input) + self.set_timer(0.05, self._focus_prompt_input) + + def _focus_prompt_input(self) -> None: + self.query_one("#prompt_input", Input).focus() + + def _move_button_focus(self, step: int) -> bool: + focused = self.focused + if not isinstance(focused, Button): + return False + buttons = list(self.query("#prompt_buttons Button")) + if len(buttons) < 2 or focused not in buttons: + return False + idx = buttons.index(focused) + buttons[(idx + step) % len(buttons)].focus() + return True + + def on_key(self, event) -> None: # noqa: ANN001 + if event.key == "left" and self._move_button_focus(-1): + event.stop() + return + if event.key == "right" and self._move_button_focus(1): + event.stop() + return + if event.key == "down" and isinstance(self.focused, Input): + buttons = list(self.query("#prompt_buttons Button")) + if buttons: + buttons[0].focus() + event.stop() + return + if event.key == "up" and isinstance(self.focused, Button): + self.query_one("#prompt_input", Input).focus() + event.stop() + + def action_cancel(self) -> None: + self.dismiss(None) + + def action_quit_app(self) -> None: + self.app.exit() + + def on_input_submitted(self, event: Input.Submitted) -> None: + self.dismiss(event.value.strip()) + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "ok": + value = self.query_one("#prompt_input", Input).value.strip() + self.dismiss(value) + return + self.dismiss(None) + + +class ConfirmScreen(ModalScreen[bool]): + AUTO_FOCUS = "#yes" + BINDINGS = [ + ("escape", "cancel", "Cancel"), + ("ctrl+q", "quit_app", "Quit"), + ("q", "quit_app", "Quit"), + ("ctrl+c", "quit_app", "Quit"), + ] + + def __init__(self, label: str) -> None: + super().__init__() + self._label = label + + def compose(self) -> ComposeResult: + with Vertical(id="prompt_dialog"): + yield Label(self._label, id="prompt_label") + with Horizontal(id="prompt_buttons"): + yield Button("Yes", id="yes", variant="primary") + yield Button("No", id="no") + + def on_mount(self) -> None: + self.query_one("#yes", Button).focus() + + def _move_button_focus(self, step: int) -> bool: + focused = self.focused + if not isinstance(focused, Button): + return False + buttons = list(self.query("#prompt_buttons Button")) + if len(buttons) < 2 or focused not in buttons: + return False + idx = buttons.index(focused) + buttons[(idx + step) % len(buttons)].focus() + return True + + def on_key(self, event) -> None: # noqa: ANN001 + if event.key == "left" and self._move_button_focus(-1): + event.stop() + return + if event.key == "right" and self._move_button_focus(1): + event.stop() + + def on_button_pressed(self, event: Button.Pressed) -> None: + self.dismiss(event.button.id == "yes") + + def action_cancel(self) -> None: + self.dismiss(False) + + def action_quit_app(self) -> None: + self.app.exit() + + +class FileShareApp(App[None]): + TITLE = "ZKAC File Share" + BINDINGS = [ + ("ctrl+q", "quit_app", "Quit"), + ("q", "quit_app", "Quit"), + ("ctrl+c", "quit_app", "Quit"), + ("c", "copy_contact", "Copy Contact"), + ] + CSS = """ + #root { + padding: 1 2; + } + #status { + border: round $accent; + padding: 0 1; + height: 4; + margin-bottom: 1; + } + #actions { + layout: grid; + grid-size: 8 1; + grid-gutter: 1 0; + height: auto; + margin-bottom: 1; + } + #actions Button { + width: 1fr; + height: 3; + text-align: center; + content-align: center middle; + } + #log { + border: round $panel; + } + #prompt_dialog { + width: 70; + height: auto; + padding: 1 2; + border: round $accent; + background: $surface; + } + #prompt_label { + margin-bottom: 1; + } + #prompt_buttons { + margin-top: 1; + height: auto; + } + """ + + def __init__(self) -> None: + super().__init__() + self.userid: str | None = None + self.identity: cli.Identity | None = None + self.server: str | None = None + self.server_pk_hex: str | None = None + self.current_bucket_id: str | None = None + self.listener: cli.P2PListener | None = None + self._action_running = False + self._log_lines: list[str] = [] + self._last_clipboard_error = "" + self._last_clipboard_backend = "" + self._last_contact_bundle = "" + self._server_key_maybe_stale_for: str | None = None + + def compose(self) -> ComposeResult: + yield Header(show_clock=True) + with Vertical(id="root"): + yield Static(id="status") + with Vertical(id="actions"): + yield Button("Login", id="login", variant="primary") + yield Button("Connect", id="connect") + yield Button("Select Bucket", id="bucket") + yield Button("Permissions", id="permissions") + yield Button("Share Permissions", id="share") + yield Button("Listen", id="listen") + yield Button("Inbox", id="inbox") + yield Button("Quit", id="quit", variant="error") + yield TextArea("", id="log", read_only=True) + yield Footer() + + def on_mount(self) -> None: + self._refresh_status() + self.write_log("Textual file-share UI ready.") + self.write_log(f"Demo ZKAC_HOME: {cli.zkac_home()}") + self.write_log("Use the action buttons to run flows.") + self.set_interval(0.8, self._poll_listener) + self._update_actions_layout() + self.query_one("#login", Button).focus() + + def on_resize(self, _event: events.Resize) -> None: + self._update_actions_layout() + + def on_unmount(self) -> None: + if self.listener: + self.listener.stop() + + def write_log(self, msg: str) -> None: + self._log_lines.extend(msg.splitlines() or [""]) + log_view = self.query_one("#log", TextArea) + log_view.load_text("\n".join(self._log_lines)) + if not isinstance(self.focused, TextArea): + log_view.scroll_end(animate=False) + + def _refresh_status(self) -> None: + listener_state = "stopped" + if self.listener and self.listener.is_running(): + listener_state = f"listening on {self.listener.address}" + elif self.listener: + listener_state = "stopped (last)" + status = ( + f"user: {self.userid or '-'}\n" + f"server: {self.server or '-'}\n" + f"bucket: {self.current_bucket_id or '-'}\n" + f"p2p-listen: {listener_state}" + ) + self.query_one("#status", Static).update(status) + + def _update_actions_layout(self) -> None: + actions = self.query_one("#actions") + if self.size.width >= 110: + actions.styles.grid_size_columns = 8 + actions.styles.grid_size_rows = 1 + else: + actions.styles.grid_size_columns = 4 + actions.styles.grid_size_rows = 2 + + def _move_action_focus(self, step: int) -> bool: + focused = self.focused + if not isinstance(focused, Button): + return False + buttons = list(self.query("#actions Button")) + if not buttons or focused not in buttons: + return False + idx = buttons.index(focused) + buttons[(idx + step) % len(buttons)].focus() + return True + + def on_key(self, event) -> None: # noqa: ANN001 + # Never intercept keys while a modal prompt/confirm screen is active. + if self.screen is not self: + return + if event.key == "left" and self._move_action_focus(-1): + event.stop() + return + if event.key == "right" and self._move_action_focus(1): + event.stop() + return + if event.key == "down" and isinstance(self.focused, Button): + self.query_one("#log", TextArea).focus() + event.stop() + return + if event.key == "up" and isinstance(self.focused, TextArea): + self.query_one("#login", Button).focus() + event.stop() + + def action_quit_app(self) -> None: + self.exit() + + async def ask(self, label: str, default: str = "") -> str | None: + return await self.push_screen_wait(PromptScreen(label, default)) + + async def confirm(self, label: str) -> bool: + return await self.push_screen_wait(ConfirmScreen(label)) + + def _require_user(self) -> cli.Identity: + if not self.userid or not self.identity: + raise RuntimeError("no current user; click Login first") + return self.identity + + def _require_server(self) -> tuple[str, str]: + if not self.server or not self.server_pk_hex: + raise RuntimeError("no current server; click Connect first") + return self.server, self.server_pk_hex + + def _open_session(self, registry_id_hex: str, role_name: str) -> fsc.FileShareSession: + ident = self._require_user() + server, server_pk_hex = self._require_server() + if role_name == "__admin__": + credential = cli.load_admin_credential(self.userid or "", registry_id_hex) + role_id = zkac.admin_role_id() + else: + credential = cli.load_credential(self.userid or "", registry_id_hex, role_name) + role_id = zkac.role_id(role_name) + return fsc.open_session( + server, + server_pk_hex=server_pk_hex, + user_transport_secret=bytes.fromhex(ident.transport_secret_hex), + registry_id_hex=registry_id_hex, + role_id=role_id, + credential=credential, + user_issuance_pk_hex=ident.issuance_pk_hex, + ) + + async def _choose_index(self, title: str, options: list[str], default: int = 1) -> int: + if not options: + raise RuntimeError(f"no options for {title}") + self.write_log(f"[{title}]") + for i, opt in enumerate(options, 1): + self.write_log(f" {i}) {opt}") + raw = await self.ask(f"{title}: pick number", str(default)) + if raw is None: + raise RuntimeError("cancelled") + if not raw.isdigit(): + raise RuntimeError("selection must be a number") + idx = int(raw) - 1 + if not 0 <= idx < len(options): + raise RuntimeError("invalid selection") + return idx + + async def _pick_owned_registry(self) -> str: + regs = cli.registry_list(self.userid or "") + if not regs: + raise RuntimeError("you do not own any registries; create one in Registry") + if self.server: + regs = [r for r in regs if r["server"] == self.server] or regs + opts = [f"{r['registry_id'][:16]}... @ {r['server']} roles={r['roles']}" for r in regs] + idx = await self._choose_index("Owned registries", opts) + return regs[idx]["registry_id"] + + def _poll_listener(self) -> None: + if not self.listener or self.listener.is_running(): + return + result = self.listener.parse_received() + if result is not None: + self.write_log( + f"[listener] received credential: registry={result['registry_id'][:16]}... " + f"role={result['role']}" + ) + self.listener = None + self._refresh_status() + + def _log_exception(self, exc: BaseException) -> None: + self.write_log(f"error: {exc}") + msg = str(exc).lower() + if self.server and any(s in msg for s in ("auth failed", "handshake", "public key", "server key")): + self._server_key_maybe_stale_for = self.server + self.write_log( + "[hint] server key may be stale. Use Connect to update the pinned transport key." + ) + if "--debug" in sys.argv: + self.write_log(traceback.format_exc()) + + def on_button_pressed(self, event: Button.Pressed) -> None: + action = event.button.id or "" + if action == "quit": + self.exit() + return + if self._action_running: + self.write_log("Another action is still running; wait or press q to quit.") + return + self._action_running = True + self.run_worker(self._run_action(action), exclusive=True, thread=False) + + async def _run_action(self, action: str) -> None: + try: + if action == "login": + await self.menu_login() + elif action == "connect": + await self.menu_connect() + elif action == "bucket": + await self.menu_buckets() + elif action == "permissions": + await self._bucket_set_masks() + elif action == "share": + await self.menu_share() + elif action == "listen": + await self.menu_listen() + elif action == "inbox": + await self.menu_inbox() + self._refresh_status() + except Exception as exc: # noqa: BLE001 + self._log_exception(exc) + finally: + self._action_running = False + + async def menu_login(self) -> None: + users = cli.list_local_users() + userid = "" + if users: + self.write_log("[login] local users:") + for i, u in enumerate(users, 1): + self.write_log(f" {i}) {u}") + pick = await self.ask("Select number or type new userid") + if pick is None or not pick: + return + if pick.isdigit() and 1 <= int(pick) <= len(users): + userid = users[int(pick) - 1] + else: + userid = pick + else: + typed = await self.ask("No local users found. New userid") + if not typed: + return + userid = typed + if not cli.user_exists(userid): + ok = await self.confirm(f"Create user '{userid}' via zkac-node user create?") + if not ok: + return + cli.create_user(userid).raise_for_status() + self.write_log(f"[login] created user {userid}") + self.userid = userid + self.identity = cli.get_identity(userid, peer=None) + self.write_log(f"[login] logged in as {userid}") + self.write_log(f" issuance pk: {self.identity.issuance_pk_hex}") + self.write_log(f" transport pk: {self.identity.transport_pk_hex}") + + async def menu_identity(self) -> None: + ident = self._require_user() + peer = await self.ask("Optional peer host:port for contact bundle") + contact = cli.show_user_contact(ident.userid, peer=peer or None) + self.write_log("[identity]") + self.write_log(f" userid: {ident.userid}") + self.write_log(f" issuance pk: {ident.issuance_pk_hex}") + self.write_log(f" transport pk: {ident.transport_pk_hex}") + self.write_log(" contact bundle:") + self.write_log(f" {contact}") + self._last_contact_bundle = contact + if self._copy_to_clipboard(contact): + self.write_log( + f" copied contact bundle to clipboard via {self._last_clipboard_backend}" + ) + else: + self.write_log( + " warning: could not copy contact bundle to clipboard" + f" ({self._last_clipboard_error})" + ) + + async def menu_connect(self) -> None: + ident = self._require_user() + server = await self.ask("File-share server host:port", self.server or "127.0.0.1:9879") + if server is None or not server: + return + pinned = cli.load_pinned_server_key(ident.userid, server) + server_pk_hex: str + if pinned and self._server_key_maybe_stale_for != server: + server_pk_hex = pinned + self.write_log(f"[connect] reused pinned key for {server} ({server_pk_hex[:16]}...)") + else: + prompt = "Server transport public key hex" + if pinned: + prompt = "Server key may be stale; enter updated transport public key hex" + server_pk_hex = await self.ask(prompt, pinned or self.server_pk_hex or "") + if server_pk_hex is None or not server_pk_hex: + return + cli.server_pin(ident.userid, server, server_pk_hex).raise_for_status() + self.write_log(f"[connect] pinned {server} = {server_pk_hex[:16]}...") + self._server_key_maybe_stale_for = None + + self.server = server + self.server_pk_hex = server_pk_hex + + async def menu_buckets(self) -> None: + op = await self.ask("Select bucket: l=list/select, c=create new", "l") + if op is None: + return + if op == "c": + await self._bucket_create() + return + await self._bucket_select_from_all_access() + + async def _bucket_select_from_all_access(self) -> None: + ident = self._require_user() + server, _ = self._require_server() + inventory: dict[str, set[str]] = {} + + # Owned/admin buckets. + for reg in cli.registry_list(ident.userid): + if reg["server"] != server: + continue + rid = reg["registry_id"] + if not cli.is_registry_admin(ident.userid, rid): + continue + try: + with self._open_session(rid, "__admin__") as sess: + for bid in sess.bucket_list_owned(): + inventory.setdefault(bid, set()).add("owner") + except Exception as exc: # noqa: BLE001 + self.write_log(f"[bucket] skip admin bucket list for {rid[:16]}... ({exc})") + + # Buckets visible via role credentials. + for cred in cli.credentials_list(ident.userid): + rid = cred["registry_id"] + role = cred["role"] + try: + with self._open_session(rid, role) as sess: + for bid in sess.fs_buckets(): + inventory.setdefault(bid, set()).add(f"role:{role}") + except Exception as exc: # noqa: BLE001 + self.write_log( + f"[bucket] skip credential registry={rid[:16]}... role={role} ({exc})" + ) + + if not inventory: + self.write_log("[bucket] no owned or permitted buckets yet") + return + + bucket_ids = sorted(inventory.keys()) + options = [ + f"{bid} access={sorted(inventory[bid])}" + for bid in bucket_ids + ] + default = 1 + if self.current_bucket_id and self.current_bucket_id in bucket_ids: + default = bucket_ids.index(self.current_bucket_id) + 1 + idx = await self._choose_index("Buckets", options, default=default) + selected = bucket_ids[idx] + self.current_bucket_id = selected + if selected in fsc.list_manifests(ident.userid): + self.write_log(f"[bucket] selected {selected} (locally managed)") + else: + self.write_log(f"[bucket] selected {selected} (remote-only access)") + + async def _bucket_create(self) -> None: + ident = self._require_user() + rid = await self._pick_or_create_registry_for_bucket() + folder_str = await self.ask("Path to folder to share") + if folder_str is None or not folder_str: + return + folder = Path(folder_str).expanduser().resolve() + if not folder.is_dir(): + raise RuntimeError(f"not a directory: {folder}") + files = fsc.flatten_folder(folder) + if not files: + raise RuntimeError("no shareable files found (empty or hidden only)") + self.write_log(f"[bucket-create] flattened {len(files)} files") + for i, p in enumerate(files): + self.write_log(f" [{i:>3}] {p.relative_to(folder)}") + + roles_meta = next( + (r for r in cli.registry_list(ident.userid) if r["registry_id"] == rid), + None, + ) + if roles_meta is None: + raise RuntimeError("registry metadata not found locally") + masks: dict[str, str] = {} + for role in roles_meta["roles"]: + raw = await self.ask( + f"Mask for role '{role}' ({len(files)} bits)", + "1" * len(files), + ) + if raw is None: + return + masks[role] = fsc.normalize_mask(raw, len(files)) + + with self._open_session(rid, "__admin__") as sess: + self.write_log("[bucket-create] uploading encrypted blobs...") + manifest = fsc.upload_bucket( + sess, + folder, + server=self.server or "", + registry_id_hex=rid, + ) + manifest.role_masks = masks + with self._open_session(rid, "__admin__") as sess: + fsc.apply_role_masks_to_server(sess, manifest) + self._log_server_acl_summary(sess, manifest) + fsc.save_manifest(ident.userid, manifest) + self.current_bucket_id = manifest.bucket_id + self.write_log(f"[bucket-create] uploaded {manifest.bucket_id} ({len(manifest.files)} files)") + + async def _select_local_manifest(self) -> fsc.BucketManifest: + ident = self._require_user() + bids = fsc.list_manifests(ident.userid) + if not bids: + raise RuntimeError("no local bucket manifests") + options: list[str] = [] + for bid in bids: + man = fsc.load_manifest(ident.userid, bid) + options.append( + f"{bid[:16]}... files={len(man.files)} roles={sorted(man.role_masks.keys())}" + ) + default = 1 + if self.current_bucket_id and self.current_bucket_id in bids: + default = bids.index(self.current_bucket_id) + 1 + idx = await self._choose_index("Select bucket", options, default=default) + return fsc.load_manifest(ident.userid, bids[idx]) + + async def _bucket_set_masks(self) -> None: + ident = self._require_user() + manifest = await self._select_local_manifest() + self.write_log(f"[bucket-masks] {manifest.bucket_id} has {len(manifest.files)} files") + for i, f in enumerate(manifest.files): + self.write_log(f" [{i:>3}] {f.rel_path}") + roles = sorted(manifest.role_masks.keys() or []) + if not roles: + roles_raw = await self.ask("Comma-separated role names", "viewer,editor") + if not roles_raw: + return + roles = [r.strip() for r in roles_raw.split(",") if r.strip()] + for role in roles: + current = manifest.role_masks.get(role, "1" * len(manifest.files)) + new = await self.ask(f"Mask for role '{role}'", current) + if new is None: + return + manifest.role_masks[role] = fsc.normalize_mask(new, len(manifest.files)) + with self._open_session(manifest.registry_id_hex, "__admin__") as sess: + fsc.apply_role_masks_to_server(sess, manifest) + self._log_server_acl_summary(sess, manifest) + fsc.save_manifest(ident.userid, manifest) + self.current_bucket_id = manifest.bucket_id + self.write_log("[permissions] bucket permissions updated") + + async def _pick_or_create_registry_for_bucket(self) -> str: + ident = self._require_user() + server, _ = self._require_server() + regs = [r for r in cli.registry_list(ident.userid) if r["server"] == server] + if regs: + options = [ + f"use {r['registry_id'][:16]}... roles={r['roles']}" + for r in regs + ] + options.append("create new registry for this server") + idx = await self._choose_index("Bucket auth profile", options) + if idx < len(regs): + return regs[idx]["registry_id"] + roles_raw = await self.ask("No auth profile yet. Create roles", "viewer,editor") + if not roles_raw: + raise RuntimeError("roles required to create bucket auth profile") + roles = [r.strip() for r in roles_raw.split(",") if r.strip()] + if not roles: + raise RuntimeError("no roles provided") + rid = cli.registry_create(ident.userid, server, roles) + self.write_log(f"[bucket] created auth profile {rid} roles={roles}") + return rid + + async def menu_share(self) -> None: + ident = self._require_user() + manifest = await self._select_local_manifest() + if not manifest.role_masks: + raise RuntimeError("set role masks first from Buckets") + roles = sorted(manifest.role_masks.keys()) + idx = await self._choose_index("Share role", roles) + role = roles[idx] + contact = await self.ask("Recipient contact bundle") + if contact is None or not contact: + return + result = fscred.grant_role_p2p( + ident.userid, + manifest.server, + manifest.registry_id_hex, + role, + contact, + ) + self.write_log(f"[share] issued role '{role}' to peer {result['peer']}") + recipient_pk_hex = _parse_contact_issuance_pk(contact) + with self._open_session(manifest.registry_id_hex, "__admin__") as sess: + fsc.apply_role_masks_to_server(sess, manifest) + self._log_server_acl_summary(sess, manifest) + fsc.push_role_grant(sess, manifest, role, recipient_pk_hex) + fsc.save_manifest(ident.userid, manifest) + self.current_bucket_id = manifest.bucket_id + self.write_log(f"[share] pushed bucket grant to {recipient_pk_hex[:16]}...") + + async def menu_listen(self) -> None: + ident = self._require_user() + if self.listener and self.listener.is_running(): + keep = await self.confirm( + f"Listener already running on {self.listener.address}. Stop it?" + ) + if keep: + self.listener.stop() + self.listener = None + else: + return + host = await self.ask("Bind host", "127.0.0.1") + if host is None or not host: + return + raw_port = await self.ask("Optional bind port (blank = random)", "") + if raw_port is None: + return + port = 0 + if raw_port.strip(): + if not raw_port.isdigit(): + raise RuntimeError("port must be a number or blank") + port = int(raw_port) + if not 1 <= port <= 65535: + raise RuntimeError("port out of range") + listener = cli.P2PListener(ident.userid, host=host, port=port) + listener.start() + time.sleep(0.3) + if not listener.is_running(): + self.write_log(f"[listen] listener exited:\n{listener.output()}") + return + contact = cli.show_user_contact(ident.userid, peer=listener.address) + self.listener = listener + self.write_log(f"[listen] listening on {listener.address} timeout={listener.timeout_s:.0f}s") + self.write_log("Share this contact bundle out-of-band:") + self.write_log(contact) + self._last_contact_bundle = contact + if self._copy_to_clipboard(contact): + self.write_log( + f"[listen] copied contact bundle to clipboard via {self._last_clipboard_backend}" + ) + else: + self.write_log( + "[listen] warning: could not copy contact bundle to clipboard" + f" ({self._last_clipboard_error})" + ) + + async def menu_inbox(self) -> None: + ident = self._require_user() + creds: list[dict[str, str]] = list(cli.credentials_list(ident.userid)) + for reg in cli.registry_list(ident.userid): + rid = reg["registry_id"] + if cli.is_registry_admin(ident.userid, rid): + creds.append({"registry_id": rid, "role": "__admin__"}) + if not creds: + self.write_log("[inbox] no local credentials; ask an admin to grant one") + return + unique: dict[tuple[str, str], dict[str, str]] = {} + for c in creds: + unique[(c["registry_id"], c["role"])] = c + creds = list(unique.values()) + opts = [f"registry={c['registry_id'][:16]}... role={c['role']}" for c in creds] + idx = await self._choose_index("Credential for inbox", opts) + cred = creds[idx] + with self._open_session(cred["registry_id"], cred["role"]) as sess: + who = sess.whoami() + self.write_log( + f"[inbox] authenticated registry={who['registry_id'][:16]}... " + f"role_id={who['role_id'][:16]}..." + ) + if cred["role"] == "__admin__": + bids = sess.bucket_list_owned() + else: + bids = sess.fs_buckets() + if not bids: + self.write_log("[inbox] no accessible buckets") + return + self.write_log("[inbox] accessible buckets:") + for i, bid in enumerate(bids, 1): + self.write_log(f" {i}) {bid}") + pick = await self.ask("Download bucket number, 'all', or blank to skip", "1") + if pick is None or not pick: + return + if pick == "all": + targets = list(range(len(bids))) + elif pick.isdigit(): + targets = [int(pick) - 1] + else: + raise RuntimeError("invalid selection") + for ti in targets: + if not 0 <= ti < len(bids): + raise RuntimeError("bucket index out of range") + out_default = str(Path.cwd() / "fs_inbox") + out = await self.ask("Download root folder", out_default) + if out is None or not out: + return + out_root = Path(out).expanduser().resolve() + for ti in targets: + bid = bids[ti] + bucket_dir = out_root / bid + if cred["role"] == "__admin__": + result = self._download_owned_bucket_as_admin( + sess=sess, + userid=ident.userid, + bucket_id=bid, + output_dir=bucket_dir, + ) + else: + result = fsc.download_bucket( + sess, + bid, + issuance_secret_hex=ident.issuance_secret_hex, + output_dir=bucket_dir, + ) + self.write_log( + f"[inbox] downloaded {bid} -> {bucket_dir} " + f"({len(result['files_written'])} files)" + ) + + def _download_owned_bucket_as_admin( + self, + *, + sess: fsc.FileShareSession, + userid: str, + bucket_id: str, + output_dir: Path, + ) -> dict: + manifest = fsc.load_manifest(userid, bucket_id) + output_dir.mkdir(parents=True, exist_ok=True) + written: list[str] = [] + for entry in manifest.files: + ciphertexts = [sess.fs_get_blob(bucket_id, c.blob_id) for c in entry.chunks] + plaintext = fsc.decrypt_file_from_blobs(entry, ciphertexts) + out_path = output_dir / entry.rel_path + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_bytes(plaintext) + written.append(str(out_path)) + return {"files_written": written} + + def _log_server_acl_summary(self, sess: fsc.FileShareSession, manifest: fsc.BucketManifest) -> None: + for role_name, raw_mask in sorted(manifest.role_masks.items()): + expected = sum(1 for bit in fsc.normalize_mask(raw_mask, len(manifest.files)) if bit == "1") + role_id_hex = zkac.role_id(role_name).hex() + acl_meta = sess.bucket_get_role_acl(manifest.bucket_id, role_id_hex) + allowed_blob_ids = list(acl_meta.get("allowed_blob_ids", [])) + acl_version = int(acl_meta.get("version", 0)) + self.write_log( + f"[permissions] server ACL role={role_name} " + f"version={acl_version} files(expected)={expected} blobs(server)={len(allowed_blob_ids)}" + ) + + def _copy_to_clipboard(self, text: str) -> bool: + text = text.strip() + if not text: + self._last_clipboard_error = "empty text" + self._last_clipboard_backend = "" + return False + is_st = os.environ.get("TERM", "").startswith("st") + + # In st, prefer explicit system clipboard tools; Textual clipboard often reports + # success but doesn't reach the desktop clipboard. + if not is_st: + try: + self.copy_to_clipboard(text) + self._last_clipboard_error = "" + self._last_clipboard_backend = "textual" + return True + except Exception: + self._last_clipboard_error = "Textual clipboard unavailable" + + def _run_clip(cmd: list[str], payload: str) -> subprocess.CompletedProcess[str] | None: + try: + return subprocess.run( + cmd, + input=payload, + text=True, + capture_output=True, + timeout=0.8, + ) + except subprocess.TimeoutExpired: + self._last_clipboard_error = f"{cmd[0]} timed out" + return None + except Exception as exc: + self._last_clipboard_error = f"{cmd[0]} failed: {exc}" + return None + + # Wayland + if shutil.which("wl-copy") and os.environ.get("WAYLAND_DISPLAY"): + p = _run_clip(["wl-copy"], text) + if p and p.returncode == 0: + self._last_clipboard_error = "" + self._last_clipboard_backend = "wl-copy" + return True + if p: + self._last_clipboard_error = (p.stderr or p.stdout or "wl-copy failed").strip() + + # X11: prefer xsel first (often exits faster), then xclip. + if shutil.which("xsel") and os.environ.get("DISPLAY"): + p1 = _run_clip(["xsel", "--clipboard", "--input"], text) + p2 = _run_clip(["xsel", "--primary", "--input"], text) + if (p1 and p1.returncode == 0) or (p2 and p2.returncode == 0): + self._last_clipboard_error = "" + self._last_clipboard_backend = "xsel" + return True + if p1 or p2: + self._last_clipboard_error = ( + (p1.stderr if p1 else "") + or (p2.stderr if p2 else "") + or (p1.stdout if p1 else "") + or (p2.stdout if p2 else "") + or "xsel failed" + ).strip() + + if shutil.which("xclip") and os.environ.get("DISPLAY"): + p1 = _run_clip(["xclip", "-selection", "clipboard", "-in"], text) + p2 = _run_clip(["xclip", "-selection", "primary", "-in"], text) + if (p1 and p1.returncode == 0) or (p2 and p2.returncode == 0): + self._last_clipboard_error = "" + self._last_clipboard_backend = "xclip" + return True + if p1 or p2: + self._last_clipboard_error = ( + (p1.stderr if p1 else "") + or (p2.stderr if p2 else "") + or (p1.stdout if p1 else "") + or (p2.stdout if p2 else "") + or "xclip failed" + ).strip() + + if shutil.which("pbcopy"): + p = _run_clip(["pbcopy"], text) + if p and p.returncode == 0: + self._last_clipboard_error = "" + self._last_clipboard_backend = "pbcopy" + return True + if p: + self._last_clipboard_error = (p.stderr or p.stdout or "pbcopy failed").strip() + + # Final fallback for terminals that support OSC52 clipboard control. + try: + encoded = base64.b64encode(text.encode("utf-8")).decode("ascii") + sys.__stdout__.write(f"\033]52;c;{encoded}\a") + sys.__stdout__.flush() + self._last_clipboard_error = "" + self._last_clipboard_backend = "osc52" + return True + except Exception as exc: + self._last_clipboard_error = f"OSC52 failed: {exc}" + self._last_clipboard_backend = "" + + if is_st: + self._last_clipboard_error = ( + "st detected; install xsel/xclip and ensure DISPLAY is set" + ) + return False + + def action_copy_contact(self) -> None: + if not self._last_contact_bundle: + self.write_log("[copy-contact] no contact bundle generated yet") + return + if self._copy_to_clipboard(self._last_contact_bundle): + self.write_log( + f"[copy-contact] copied latest contact bundle via {self._last_clipboard_backend}" + ) + return + self.write_log( + "[copy-contact] failed to copy latest contact bundle" + f" ({self._last_clipboard_error})" + ) + + +def _parse_contact_issuance_pk(bundle: str) -> str: + s = bundle.strip() + raw = base64.urlsafe_b64decode((s + "=" * (-len(s) % 4)).encode()) + data = json.loads(raw.decode("utf-8")) + pk = data.get("issuance_pk_hex") + if not isinstance(pk, str) or len(pk) != 64: + raise RuntimeError("contact bundle missing issuance_pk_hex") + return pk + + +def main() -> None: + FileShareApp().run() + + +if __name__ == "__main__": + main() diff --git a/demo/fs_data/registries/519bc59c917f122245d5a6a131ec42f5e3b57ae0b73c8b99a4e0cafc9c3eedab.state b/demo/fs_data/registries/519bc59c917f122245d5a6a131ec42f5e3b57ae0b73c8b99a4e0cafc9c3eedab.state new file mode 100644 index 0000000000000000000000000000000000000000..1a99c493a6b24f1b4617648cc53784357ebefdf6 GIT binary patch literal 628 zcmWHXeRR&mdLbp(tIHM|zH$2ccx%;z?Ka&rmpnN2XO7+5)r<_pfdrtk)`+K)J<;J; zMS40<`t_QG%#UGv-BF!C<4W$H9?!2SrMF|v77J~AW^5QExZguJu9`XQ^S52mRkF#i zX2#m=zk4lPT=!$w6Mn}neBZ^^)cjuaXAyVD*>-KeGg0Mzk~*JsR^2=@eM7+7mD!BV<@~{2H1%j$w0-R3LI~RIOn@)WA;PiTSt4hZm)jh;p?o_BJoaoW&fuM mr%v2tU_NbmdxzWez4C{3x%v+-zyCq!vX%a~yqlZp=@tOQZ4mYV literal 0 HcmV?d00001 diff --git a/demo/fs_data/registries/a0e5ed90dafc9ddfa18197b599a63ea5bc43228a3cf1d5f64a043994951c31ea.cert b/demo/fs_data/registries/a0e5ed90dafc9ddfa18197b599a63ea5bc43228a3cf1d5f64a043994951c31ea.cert new file mode 100644 index 0000000000000000000000000000000000000000..0820bfd0e3f9fcfde8cd0c68af0775e64fe09623 GIT binary patch literal 336 zcmV-W0k8gzdY{x{Xod!7C!k79z?x5)wX5oY@J#lyc0ag8j}3REW%4I^?ix=t(W<@ifshMx zgI(?!Iy>-P^)mPWAs7|#d4A->pDAKnk34hw(m1_5C$V7J1>@2sQyS-Axzd*Z)oH;* zpZR5fY_^5Itm#^&-o5J~-?No}>B{#UFBJ=_Tkv+lD`SZAzoN=hu2G5?|UJL84-x8S{(Kc)7$BwJpq6WUErx2%1q m`&4u?TamF$Cy$HG<0mbhT59dJQ?@5fnlRVs>#WoCbPE8B0Tq$} literal 0 HcmV?d00001 diff --git a/demo/fs_data/registries/fc2c7a6c4a57247946f5bcf006fb92c0ad7469322dcef8269bc8694ecf262386.cert b/demo/fs_data/registries/fc2c7a6c4a57247946f5bcf006fb92c0ad7469322dcef8269bc8694ecf262386.cert new file mode 100644 index 0000000000000000000000000000000000000000..562c77928c7dba57e76d723011ca7f8c666f0422 GIT binary patch literal 336 zcmV-W0k8gzp-r+kZLzps#0MpiC$Mms>#IK(HZ;HS&gQ$5=;`>D>qUp4_YQ}?DdkA>B@#g+U8aR=^$ zHSRpx{)ArxA8g^g>&yod8%M{vb;@zEU0vkHmTuHKpiLM$`- z$n4FaxpLq|l4DmI#Mb_(w;e?WNzpeMU*zIsA8>_%LS#oTvI#j1Zt|!X5XeE(%aUPd zi>;qZ%*1{GR-2(c@|7s404dc}rZHA^ifAQUa5CPeTNSw63m0zkh2|8}i#D|zSi_`C i(|a%j10tq1fa4dK23+^PR{ZU zNebj+%E{lO>$rcSjakWy<^@ew8FOa7SH1QB%C05c6MLdRieFoIt(wn$cW>!Er>ksJ z{BFJOsoS-B{yW?BlT}$d)}IgVlXuqo{w(NA{oD;@9Y4D-y|c;gmz=**Jf=)0zv*eq zjh6Vxy=Dv(7rnVR^~0exoYAwlRFyCR-LSjf@X)OL*4BdNub6XE8ZC7vzAuk{f8eoA z8^_XE)kh-qa3?Ix{snxke7yQ_u8&J|uJotXT5E!h44F49WENm<$?()`vY@A10INvX AIsgCw literal 0 HcmV?d00001 diff --git a/demo/zkac_cli_adapter.py b/demo/zkac_cli_adapter.py new file mode 100644 index 0000000..ee9043f --- /dev/null +++ b/demo/zkac_cli_adapter.py @@ -0,0 +1,420 @@ +""" +Subprocess adapter around the existing ``zkac-node`` CLI. + +The demo deliberately does not import ``cli/zkac_cli/*``: every interaction +with identity, registry, grant or p2p-listen flows runs through the CLI as a +black box, exactly the way a third-party application would integrate. + +The only direct filesystem touch is reading the CLI-managed ``identity.json`` +under ``ZKAC_HOME`` (default ``~/.ZKAC-FS//`` for this demo). That file is the CLI's +public on-disk format and is needed locally to obtain the user's transport and +issuance secrets for the file-share session and role-grant decryption. +""" + +from __future__ import annotations + +import base64 +import hashlib +import json +import os +import re +import shutil +import subprocess +import sys +import threading +from dataclasses import dataclass +from pathlib import Path + +import zkac + +ROOT = Path(__file__).resolve().parents[1] +CLI_DIR = ROOT / "cli" + +DEFAULT_TIMEOUT_S = 60.0 +DEFAULT_LISTEN_TIMEOUT_S = 600.0 +DEFAULT_DEMO_ZKAC_HOME = Path.home() / ".ZKAC-FS" + + +# ── helpers ────────────────────────────────────────────────────────── + +def _b64_decode(s: str) -> bytes: + return base64.b64decode(s) + + +def zkac_home() -> Path: + return Path(os.environ.get("ZKAC_HOME", DEFAULT_DEMO_ZKAC_HOME)) + + +def user_identity_path(userid: str) -> Path: + return zkac_home() / userid / "identity.json" + + +def user_exists(userid: str) -> bool: + return user_identity_path(userid).is_file() + + +def list_local_users() -> list[str]: + home = zkac_home() + if not home.is_dir(): + return [] + return sorted(d.name for d in home.iterdir() if (d / "identity.json").is_file()) + + +def _zkac_invocation() -> tuple[list[str], dict[str, str]]: + """Resolve a runnable ``zkac-node`` command, falling back to the source tree.""" + exe = shutil.which("zkac-node") + if exe: + return [exe], {} + return [sys.executable, "-m", "zkac_cli.main"], {"PYTHONPATH": str(CLI_DIR)} + + +@dataclass +class CliResult: + rc: int + stdout: str + stderr: str + + def ok(self) -> bool: + return self.rc == 0 + + def raise_for_status(self) -> "CliResult": + if self.rc != 0: + raise RuntimeError( + f"zkac-node failed (rc={self.rc}): {self.stderr.strip() or self.stdout.strip()}" + ) + return self + + +def run_cli( + args: list[str], + *, + timeout_s: float = DEFAULT_TIMEOUT_S, + extra_env: dict[str, str] | None = None, +) -> CliResult: + prefix, env_overrides = _zkac_invocation() + env = { + **os.environ, + **env_overrides, + "ZKAC_HOME": str(zkac_home()), + **(extra_env or {}), + } + try: + p = subprocess.run( + prefix + args, + capture_output=True, + text=True, + timeout=timeout_s, + cwd=str(ROOT), + env=env, + ) + except subprocess.TimeoutExpired as exc: + return CliResult(rc=124, stdout=exc.stdout or "", stderr=f"timed out after {timeout_s}s") + return CliResult(rc=p.returncode, stdout=p.stdout, stderr=p.stderr) + + +# ── identity ───────────────────────────────────────────────────────── + +@dataclass +class Identity: + userid: str + issuance_pk_hex: str + issuance_secret_hex: str + transport_pk_hex: str + transport_secret_hex: str + grant_token_b64: str + contact_bundle: str # value of ``zkac-node user show --peer ...`` + + +def load_identity_secrets(userid: str) -> dict[str, str]: + """Read the CLI-managed ``identity.json`` (creator: ``zkac-node user create``).""" + path = user_identity_path(userid) + if not path.is_file(): + raise FileNotFoundError(f"unknown user {userid!r} (run: zkac-node user create {userid})") + data = json.loads(path.read_text()) + + def hexed(b64_field: str) -> str: + return _b64_decode(data[b64_field]).hex() + + return { + "issuance_pk_hex": hexed("issuance_public_b64"), + "issuance_secret_hex": hexed("issuance_secret_b64"), + "transport_pk_hex": hexed("transport_public_b64"), + "transport_secret_hex": hexed("transport_secret_b64"), + "grant_token_b64": data["grant_token_b64"], + } + + +def create_user(userid: str) -> CliResult: + return run_cli(["user", "create", userid]) + + +def show_user_contact(userid: str, peer: str | None = None) -> str: + args = ["user", "show", userid] + if peer: + args += ["--peer", peer] + res = run_cli(args).raise_for_status() + for line in res.stdout.splitlines(): + line = line.strip() + # contact bundle is the indented blob after "share contact:" + if line and not line.startswith(("user:", "share contact:", "issuance pk:", "p2p transport pk:", "(", "registries owned:", "credentials:", "contact peer endpoint:", "owner admin")): + if re.fullmatch(r"[A-Za-z0-9_\-]+", line): + return line + raise RuntimeError("could not parse contact bundle from `zkac-node user show`") + + +def get_identity(userid: str, peer: str | None = None) -> Identity: + secrets = load_identity_secrets(userid) + contact = show_user_contact(userid, peer=peer) + return Identity( + userid=userid, + issuance_pk_hex=secrets["issuance_pk_hex"], + issuance_secret_hex=secrets["issuance_secret_hex"], + transport_pk_hex=secrets["transport_pk_hex"], + transport_secret_hex=secrets["transport_secret_hex"], + grant_token_b64=secrets["grant_token_b64"], + contact_bundle=contact, + ) + + +# ── registry / pinning / grants ────────────────────────────────────── + +def server_pin(userid: str, server: str, server_pk_hex: str) -> CliResult: + return run_cli(["server", "pin", userid, server, "--key", server_pk_hex]) + + +def load_pinned_server_key(userid: str, server: str) -> str | None: + digest = hashlib.sha256(server.encode("utf-8")).hexdigest() + pin_file = zkac_home() / userid / "servers" / f"sha256_{digest}.json" + if not pin_file.is_file(): + return None + data = json.loads(pin_file.read_text()) + server_pk_b64 = data.get("server_public_key_b64") + if not isinstance(server_pk_b64, str): + return None + return _b64_decode(server_pk_b64).hex() + + +def registry_create(userid: str, server: str, roles: list[str]) -> str: + res = run_cli(["registry", "create", userid, server, "--roles", ",".join(roles)]).raise_for_status() + for line in res.stdout.splitlines(): + line = line.strip() + if line.startswith("registry created:"): + return line.split(":", 1)[1].strip() + raise RuntimeError(f"could not parse registry id from output:\n{res.stdout}") + + +def registry_list(userid: str) -> list[dict]: + res = run_cli(["registry", "list", userid]).raise_for_status() + out: list[dict] = [] + for line in res.stdout.splitlines(): + s = line.strip() + m = re.match( + r"^([0-9a-fA-F]+)\s+@\s+(\S+)\s+roles=\[(.*)\]\s*$", + s, + ) + if m: + roles_raw = m.group(3) + roles = [r.strip().strip("'\"") for r in roles_raw.split(",") if r.strip()] + out.append({"registry_id": m.group(1), "server": m.group(2), "roles": roles}) + return out + + +def registry_add_roles(userid: str, server: str, registry_id: str, add_roles: list[str]) -> CliResult: + return run_cli([ + "registry", "update", userid, server, + "--registry", registry_id, + "--add-roles", ",".join(add_roles), + ]).raise_for_status() + + +def credentials_list(userid: str) -> list[dict]: + """Return locally held BBS credentials as [{registry_id, role}].""" + res = run_cli(["credentials", "list", userid]).raise_for_status() + creds: list[dict] = [] + in_local = False + for line in res.stdout.splitlines(): + if line.startswith("local credentials:"): + in_local = True + continue + if not line.strip(): + continue + if line.startswith(("owner admin capability:", "registries owned:")): + in_local = False + continue + if in_local: + s = line.strip() + if s == "(none)": + continue + if ":" in s: + rid, role = s.rsplit(":", 1) + creds.append({"registry_id": rid.strip(), "role": role.strip()}) + return creds + + +def grant( + userid: str, + server: str, + registry_id: str, + role_name: str, + recipient_contact: str, +) -> CliResult: + return run_cli([ + "grant", userid, + "--server", server, + "--registry", registry_id, + "--role", role_name, + "--to", recipient_contact, + ]).raise_for_status() + + +# ── background p2p-listen ──────────────────────────────────────────── + +class P2PListener: + """Background ``zkac-node p2p-listen`` process the TUI can stop and watch.""" + + def __init__( + self, + userid: str, + host: str = "127.0.0.1", + port: int = 0, + timeout_s: float = DEFAULT_LISTEN_TIMEOUT_S, + ) -> None: + self.userid = userid + self.host = host + self.port = port if port else _pick_random_port(host) + self.timeout_s = timeout_s + self._proc: subprocess.Popen[str] | None = None + self._stdout_buf = "" + self._lock = threading.Lock() + + @property + def address(self) -> str: + return f"{self.host}:{self.port}" + + def start(self) -> None: + prefix, env_overrides = _zkac_invocation() + env = { + **os.environ, + **env_overrides, + "PYTHONUNBUFFERED": "1", + "ZKAC_HOME": str(zkac_home()), + } + args = prefix + [ + "p2p-listen", self.userid, + "--host", self.host, + "--port", str(self.port), + "--timeout", str(self.timeout_s), + ] + self._proc = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + cwd=str(ROOT), + env=env, + bufsize=1, + ) + threading.Thread(target=self._drain, daemon=True).start() + + def _drain(self) -> None: + if self._proc is None or self._proc.stdout is None: + return + for line in self._proc.stdout: + with self._lock: + self._stdout_buf += line + + def is_running(self) -> bool: + return self._proc is not None and self._proc.poll() is None + + def stop(self) -> None: + if self._proc is None: + return + if self._proc.poll() is None: + self._proc.terminate() + try: + self._proc.wait(timeout=2.0) + except subprocess.TimeoutExpired: + self._proc.kill() + self._proc.wait() + + def output(self) -> str: + with self._lock: + return self._stdout_buf + + def parse_received(self) -> dict | None: + """Extract ``{registry_id, role}`` from CLI output once a grant has been received.""" + out = self.output() + rid = role = None + for line in out.splitlines(): + s = line.strip() + if s.startswith("registry:"): + rid = s.split(":", 1)[1].strip() + elif s.startswith("role:"): + role = s.split(":", 1)[1].strip() + if rid and role: + return {"registry_id": rid, "role": role} + return None + + +def _pick_random_port(host: str) -> int: + import socket + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.bind((host, 0)) + return s.getsockname()[1] + finally: + s.close() + + +# ── credentials and admin material (CLI on-disk format, read-only) ──── + +def _user_dir(userid: str) -> Path: + return zkac_home() / userid + + +def load_credential(userid: str, registry_id_hex: str, role_name: str) -> zkac.Credential: + """Reconstruct a finalized BBS credential previously stored by ``zkac-node grant``.""" + p = _user_dir(userid) / "credentials" / f"{registry_id_hex}_{role_name}.json" + if not p.is_file(): + raise FileNotFoundError( + f"no credential for {role_name!r} on {registry_id_hex[:16]}…; " + "ask the registry admin to grant via `zkac-node grant`." + ) + data = json.loads(p.read_text()) + pk = zkac.BbsPublicKey.from_bytes(_b64_decode(data["issuer_pk_b64"])) + return zkac.Credential.finalize( + _b64_decode(data["blind_sig_b64"]), + _b64_decode(data["member_secret_b64"]), + _b64_decode(data["prover_blind_b64"]), + zkac.role_id(data["role_name"]), + int(data["epoch"]), + pk, + ) + + +def load_admin_material(userid: str, registry_id_hex: str) -> dict: + p = _user_dir(userid) / "admin" / f"{registry_id_hex}.json" + if not p.is_file(): + raise FileNotFoundError( + f"no admin material for {registry_id_hex[:16]}… under {userid!r}; " + "you are not the owner of this registry" + ) + return json.loads(p.read_text()) + + +def load_admin_credential(userid: str, registry_id_hex: str) -> zkac.Credential: + """Reconstruct the registry-admin BBS credential (role = ``admin_role_id``).""" + data = load_admin_material(userid, registry_id_hex) + pk = zkac.BbsPublicKey.from_bytes(_b64_decode(data["bbs_issuer_public_b64"])) + return zkac.Credential.finalize( + _b64_decode(data["admin_blind_sig_b64"]), + _b64_decode(data["admin_member_secret_b64"]), + _b64_decode(data["admin_prover_blind_b64"]), + zkac.admin_role_id(), + 0, + pk, + ) + + +def is_registry_admin(userid: str, registry_id_hex: str) -> bool: + return (_user_dir(userid) / "admin" / f"{registry_id_hex}.json").is_file() diff --git a/pyproject.toml b/pyproject.toml index 814343c..f0a6a64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ cli = ["zkac-node"] demo = [ "flask>=3.0", "flask-sock>=0.7", + "textual>=0.70", "zkac-node", ] dev = [ diff --git a/uv.lock b/uv.lock index 1b7645f..e1bd437 100644 --- a/uv.lock +++ b/uv.lock @@ -857,6 +857,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, ] +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/5c/f3aedc83549aae71cd52b9e9687fe896e3dc6e966ba20eba04718605d198/markdown_it_py-4.1.0.tar.gz", hash = "sha256:760e3f87b2787c044c5138a5ba107b7c2be26c03b13cc7f8fe42756b65b1df6c", size = 81613, upload-time = "2026-05-06T16:32:13.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl", hash = "sha256:d4939a62a2dd0cd9cb80a191a711ba1d39bac8ed5ef9e9966895b0171c01c46d", size = 90955, upload-time = "2026-05-06T16:32:12.184Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1053,6 +1082,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/2a/afe0193b673a79ffd2e01ad999511b7e9e6b49af02bb3759d82a78c3043d/maturin-1.13.1-py3-none-win_arm64.whl", hash = "sha256:2839024dcd65776abb4759e5bca29941971e095574162a4d335191da4be9ff24", size = 8905575, upload-time = "2026-04-09T15:14:03.891Z" }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "mistune" version = "3.2.1" @@ -1607,6 +1657,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + [[package]] name = "rpds-py" version = "0.30.0" @@ -1773,6 +1836,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "textual" +version = "8.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/1e/1eedc5bac184d00aaa5f9a99095f7e266af3ec46fa926c1051be5d358da1/textual-8.2.5.tar.gz", hash = "sha256:6c894e65a879dadb4f6cf46ddcfedb0173ff7e0cb1fe605ff7b357a597bdbc90", size = 1851596, upload-time = "2026-04-30T08:02:58.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/01/c4555f9c8a692ff83d84930150540f743ce94c89234f9e9a15ff4baba3a8/textual-8.2.5-py3-none-any.whl", hash = "sha256:247d2aa2faf222749c321f88a736247f37ee2c023604079c7490bfacddfcd4b2", size = 727050, upload-time = "2026-04-30T08:03:01.421Z" }, +] + [[package]] name = "tinycss2" version = "1.4.0" @@ -1874,6 +1954,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + [[package]] name = "wcwidth" version = "0.6.0" @@ -1931,6 +2020,7 @@ cli = [ demo = [ { name = "flask" }, { name = "flask-sock" }, + { name = "textual" }, { name = "zkac-node" }, ] dev = [ @@ -1950,6 +2040,7 @@ requires-dist = [ { name = "flask-sock", marker = "extra == 'demo'", specifier = ">=0.7" }, { name = "ipykernel", specifier = ">=6.31.0" }, { name = "maturin", marker = "extra == 'dev'", specifier = ">=1.0,<2.0" }, + { name = "textual", marker = "extra == 'demo'", specifier = ">=0.70" }, { name = "zkac-node", marker = "extra == 'cli'", editable = "cli" }, { name = "zkac-node", marker = "extra == 'demo'", editable = "cli" }, { name = "zkac-node", marker = "extra == 'dev'", editable = "cli" }, From fe68752cc7864934190e9b7f8d67d2a27de2b006 Mon Sep 17 00:00:00 2001 From: everbarry Date: Thu, 7 May 2026 22:31:37 +0200 Subject: [PATCH 2/4] demo: privacy-harden file share and add guardrail tests Harden fs auth and storage for a trustless-server model: proof-only hello, opaque tagged bucket metadata, safer connection logging, and inbox UI without raw ids. Add demo/test_demo_privacy_guardrails.py and README notes. Stop tracking demo __pycache__ and fs_data artifacts. Co-authored-by: Cursor --- demo/README.md | 8 + .../cli_web_server.cpython-314.pyc | Bin 16822 -> 0 bytes .../file_share_client.cpython-314.pyc | Bin 36061 -> 0 bytes .../file_share_credentials.cpython-314.pyc | Bin 19426 -> 0 bytes .../file_share_server.cpython-314.pyc | Bin 39288 -> 0 bytes .../file_share_smoke.cpython-314.pyc | Bin 15721 -> 0 bytes .../file_share_tui.cpython-314.pyc | Bin 70590 -> 0 bytes demo/__pycache__/server.cpython-314.pyc | Bin 43067 -> 0 bytes .../simple_cli_client.cpython-314.pyc | Bin 8230 -> 0 bytes .../simple_i2p_server.cpython-314.pyc | Bin 6746 -> 0 bytes .../trustless_server.cpython-314.pyc | Bin 16206 -> 0 bytes .../zkac_admin_serve.cpython-314.pyc | Bin 13634 -> 0 bytes .../zkac_cli_adapter.cpython-314.pyc | Bin 27584 -> 0 bytes demo/file_share_client.py | 153 +++++--- demo/file_share_credentials.py | 17 + demo/file_share_server.py | 366 +++++++++++++----- demo/file_share_smoke.py | 50 +-- demo/file_share_tui.py | 8 +- ...42f5e3b57ae0b73c8b99a4e0cafc9c3eedab.state | Bin 628 -> 0 bytes ...63ea5bc43228a3cf1d5f64a043994951c31ea.cert | Bin 336 -> 0 bytes ...3ea5bc43228a3cf1d5f64a043994951c31ea.state | Bin 628 -> 0 bytes ...b92c0ad7469322dcef8269bc8694ecf262386.cert | Bin 336 -> 0 bytes ...92c0ad7469322dcef8269bc8694ecf262386.state | Bin 488 -> 0 bytes demo/test_demo_privacy_guardrails.py | 192 +++++++++ 24 files changed, 612 insertions(+), 182 deletions(-) delete mode 100644 demo/__pycache__/cli_web_server.cpython-314.pyc delete mode 100644 demo/__pycache__/file_share_client.cpython-314.pyc delete mode 100644 demo/__pycache__/file_share_credentials.cpython-314.pyc delete mode 100644 demo/__pycache__/file_share_server.cpython-314.pyc delete mode 100644 demo/__pycache__/file_share_smoke.cpython-314.pyc delete mode 100644 demo/__pycache__/file_share_tui.cpython-314.pyc delete mode 100644 demo/__pycache__/server.cpython-314.pyc delete mode 100644 demo/__pycache__/simple_cli_client.cpython-314.pyc delete mode 100644 demo/__pycache__/simple_i2p_server.cpython-314.pyc delete mode 100644 demo/__pycache__/trustless_server.cpython-314.pyc delete mode 100644 demo/__pycache__/zkac_admin_serve.cpython-314.pyc delete mode 100644 demo/__pycache__/zkac_cli_adapter.cpython-314.pyc delete mode 100644 demo/fs_data/registries/519bc59c917f122245d5a6a131ec42f5e3b57ae0b73c8b99a4e0cafc9c3eedab.state delete mode 100644 demo/fs_data/registries/a0e5ed90dafc9ddfa18197b599a63ea5bc43228a3cf1d5f64a043994951c31ea.cert delete mode 100644 demo/fs_data/registries/a0e5ed90dafc9ddfa18197b599a63ea5bc43228a3cf1d5f64a043994951c31ea.state delete mode 100644 demo/fs_data/registries/fc2c7a6c4a57247946f5bcf006fb92c0ad7469322dcef8269bc8694ecf262386.cert delete mode 100644 demo/fs_data/registries/fc2c7a6c4a57247946f5bcf006fb92c0ad7469322dcef8269bc8694ecf262386.state create mode 100644 demo/test_demo_privacy_guardrails.py diff --git a/demo/README.md b/demo/README.md index c46cc51..4509655 100644 --- a/demo/README.md +++ b/demo/README.md @@ -10,6 +10,7 @@ This folder contains only the self-contained Textual file-share demo. - `demo/file_share_tui.py`: Textual UI. - `demo/zkac_cli_adapter.py`: subprocess bridge to `zkac-node`. - `demo/file_share_smoke.py`: end-to-end smoke test. +- `demo/test_demo_privacy_guardrails.py`: pytest privacy regressions for the demo. ## Run @@ -38,4 +39,11 @@ local ZKAC usage. ```bash uv run python demo/file_share_smoke.py +pytest demo/test_demo_privacy_guardrails.py ``` + +## Future Work + +- Further reduce at-rest metadata by removing persisted raw role-id indexes used + for proof candidate discovery after restart, while preserving reliable auth + recovery semantics. diff --git a/demo/__pycache__/cli_web_server.cpython-314.pyc b/demo/__pycache__/cli_web_server.cpython-314.pyc deleted file mode 100644 index 46cba9e75c43160bfa869bec64b1b481a93571a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16822 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 diff --git a/demo/__pycache__/file_share_client.cpython-314.pyc b/demo/__pycache__/file_share_client.cpython-314.pyc deleted file mode 100644 index 71dc4c1ff95493a16fc4119b4f4b53d02a820386..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36061 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 WZXE>NyEX`2cdLl$+_P!)>Hi0Dzt)-n diff --git a/demo/__pycache__/file_share_credentials.cpython-314.pyc b/demo/__pycache__/file_share_credentials.cpython-314.pyc deleted file mode 100644 index 9d871b0de873e11a1d34cee8d738aefe8e69efff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19426 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

k&q(i|6`UO~!bf~12%!*tg8Pi3jkztDHkOY+7 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 diff --git a/demo/__pycache__/file_share_smoke.cpython-314.pyc b/demo/__pycache__/file_share_smoke.cpython-314.pyc deleted file mode 100644 index 66370b43685b49b2a02f672336b18ce6adf377df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15721 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

$oB_`HE3V`!xB4mv74n@tUpUz`?Vag<&3D;a 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 KPG8RY|NjEl@l87b diff --git a/demo/__pycache__/simple_cli_client.cpython-314.pyc b/demo/__pycache__/simple_cli_client.cpython-314.pyc deleted file mode 100644 index 934d6f01d63df0a96b1dd4dea5846bb4b8c0874f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8230 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 btRz{63(c1AxVL=4`QJMAz|C-5shIx&=>k`O diff --git a/demo/__pycache__/simple_i2p_server.cpython-314.pyc b/demo/__pycache__/simple_i2p_server.cpython-314.pyc deleted file mode 100644 index 1b1c90d0fc3606e4310bf549eb8ed76c3bd41cbb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6746 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 LTbU2H&=CJ$VZ;N9 diff --git a/demo/__pycache__/zkac_admin_serve.cpython-314.pyc b/demo/__pycache__/zkac_admin_serve.cpython-314.pyc deleted file mode 100644 index bb4836d3bf6193ec093976c4097820daf7660c20..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13634 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

!7zwuf+t+ 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 diff --git a/demo/file_share_client.py b/demo/file_share_client.py index 14224cb..9f71228 100644 --- a/demo/file_share_client.py +++ b/demo/file_share_client.py @@ -6,7 +6,7 @@ harness. The library performs: * deterministic folder flattening and per-file content-key generation, * per-role visibility bitmasks over the flattened index, -* opaque blob and per-recipient role-grant uploads to ``file_share_server.py``, +* opaque blob uploads to ``file_share_server.py``, * role-credential authenticated download + decrypt against the same server. Authenticated sessions piggy-back on the ZKAC TCP framing/handshake from @@ -98,7 +98,7 @@ class BucketManifest: registry_id_hex: str files: list[FileEntry] role_masks: dict[str, str] = field(default_factory=dict) # role -> bitmask string ('0'/'1') - recipients: list[dict] = field(default_factory=list) # [{role_name, issuance_pk_hex}] + recipients: list[dict] = field(default_factory=list) # local audit log only (no server identity binding) def to_dict(self) -> dict: return { @@ -311,10 +311,17 @@ class FileShareSession: "ciphertext_b64": _b64(ciphertext), }) + def bucket_set_role_acl(self, bucket_id: str, role_id_hex: str, allowed_blob_ids: list[str]) -> None: + self._call({ + "cmd": "bucket_set_role_acl", + "bucket_id": bucket_id, + "role_id_hex": role_id_hex, + "allowed_blob_ids": allowed_blob_ids, + }) + def bucket_put_role_grant( self, bucket_id: str, - recipient_pk_hex: str, role_id_hex: str, acl_version: int, eph_pk_b64: str, @@ -323,21 +330,12 @@ class FileShareSession: self._call({ "cmd": "bucket_put_role_grant", "bucket_id": bucket_id, - "recipient_pk_hex": recipient_pk_hex, "role_id_hex": role_id_hex, "acl_version": int(acl_version), "eph_pk_b64": eph_pk_b64, "ciphertext_b64": ciphertext_b64, }) - def bucket_set_role_acl(self, bucket_id: str, role_id_hex: str, allowed_blob_ids: list[str]) -> None: - self._call({ - "cmd": "bucket_set_role_acl", - "bucket_id": bucket_id, - "role_id_hex": role_id_hex, - "allowed_blob_ids": allowed_blob_ids, - }) - def bucket_get_role_acl(self, bucket_id: str, role_id_hex: str) -> dict: return self._call({ "cmd": "bucket_get_role_acl", @@ -377,7 +375,6 @@ def open_session( registry_id_hex: str, role_id: bytes, credential: "zkac.Credential", - user_issuance_pk_hex: str, connect_timeout_s: float = DEFAULT_CONNECT_TIMEOUT_S, ) -> FileShareSession: """Connect, anonymous-handshake, then present a BBS+ proof bound to the transcript.""" @@ -393,10 +390,7 @@ def open_session( bbs_auth_proof = bytes(credential.present(transcript_hash)) framed.send(json.dumps({ "op": "fs", - "registry_id": registry_id_hex, - "role_id": role_id.hex(), "bbs_auth_b64": _b64(bbs_auth_proof), - "issuance_pk_hex": user_issuance_pk_hex, }).encode("utf-8")) hello = json.loads(framed.recv()) if hello.get("error"): @@ -436,40 +430,6 @@ def upload_bucket( ) -def push_role_grant( - sess: FileShareSession, - manifest: BucketManifest, - role_name: str, - recipient_issuance_pk_hex: str, -) -> None: - """Encrypt the per-role visible-file subset to ``recipient`` and store on server.""" - if role_name not in manifest.role_masks: - raise RuntimeError(f"role {role_name!r} has no visibility mask in this bucket") - mask = normalize_mask(manifest.role_masks[role_name], len(manifest.files)) - visible = files_visible_to_mask(manifest.files, mask) - role_id_hex = zkac.role_id(role_name).hex() - acl_meta = sess.bucket_get_role_acl(manifest.bucket_id, role_id_hex) - acl_version = int(acl_meta.get("version", 0)) - if acl_version <= 0: - raise RuntimeError("server ACL missing for role; apply permissions before sharing") - payload = encode_grant_payload( - manifest.bucket_id, role_name, manifest.server, manifest.registry_id_hex, visible, - ) - eph_pk_b64, ct_b64 = encrypt_grant_to_recipient(payload, recipient_issuance_pk_hex) - sess.bucket_put_role_grant( - manifest.bucket_id, - recipient_issuance_pk_hex, - role_id_hex, - acl_version, - eph_pk_b64, - ct_b64, - ) - manifest.recipients.append({ - "role_name": role_name, - "issuance_pk_hex": recipient_issuance_pk_hex, - }) - - def apply_role_masks_to_server(sess: FileShareSession, manifest: BucketManifest) -> None: """Push per-role blob ACLs so server enforces mask on fs_get_blob.""" if not manifest.role_masks: @@ -485,16 +445,63 @@ def apply_role_masks_to_server(sess: FileShareSession, manifest: BucketManifest) ) +def push_role_grant( + sess: FileShareSession, + manifest: BucketManifest, + role_name: str, + recipient_issuance_pk_hex: str, +) -> None: + """Upload anonymous encrypted role grant envelope (no recipient identifier stored).""" + if role_name not in manifest.role_masks: + raise RuntimeError(f"role {role_name!r} has no visibility mask in this bucket") + mask = normalize_mask(manifest.role_masks[role_name], len(manifest.files)) + visible = files_visible_to_mask(manifest.files, mask) + role_id_hex = zkac.role_id(role_name).hex() + acl_meta = sess.bucket_get_role_acl(manifest.bucket_id, role_id_hex) + acl_version = int(acl_meta.get("version", 0)) + if acl_version <= 0: + raise RuntimeError("server ACL missing for role; apply permissions before sharing") + payload = encode_grant_payload( + manifest.bucket_id, role_name, manifest.server, manifest.registry_id_hex, visible, + ) + eph_pk_b64, ct_b64 = encrypt_grant_to_recipient(payload, recipient_issuance_pk_hex) + sess.bucket_put_role_grant( + manifest.bucket_id, + role_id_hex, + acl_version, + eph_pk_b64, + ct_b64, + ) + manifest.recipients.append({ + "role_name": role_name, + "recipient_hint": recipient_issuance_pk_hex[:16], + }) + + def download_bucket( sess: FileShareSession, bucket_id: str, *, issuance_secret_hex: str, + role_grant_payload: dict | None = None, output_dir: Path, ) -> dict: - """Fetch + decrypt every file the auth'd role has access to.""" - grant = sess.fs_get_role_grant(bucket_id) - payload = decrypt_grant_for_recipient(grant["eph_pk_b64"], grant["ciphertext_b64"], issuance_secret_hex) + """Fetch + decrypt files from a role grant, optionally fetched from server.""" + payload = role_grant_payload + if payload is None: + grants = sess.fs_get_role_grant(bucket_id).get("grants", []) + for grant in grants: + try: + payload = decrypt_grant_for_recipient( + grant["eph_pk_b64"], + grant["ciphertext_b64"], + issuance_secret_hex, + ) + break + except Exception: + continue + if payload is None: + raise RuntimeError("no decryptable role grant for this credential") if payload.get("bucket_id") != bucket_id: raise RuntimeError("role grant bucket_id mismatch") output_dir.mkdir(parents=True, exist_ok=True) @@ -510,6 +517,46 @@ def download_bucket( return {"role_name": payload.get("role_name"), "files_written": written} +def _received_grant_path(userid: str, registry_id_hex: str, role_name: str, bucket_id: str) -> Path: + return state_dir(userid) / "received_grants" / registry_id_hex / role_name / f"{bucket_id}.json" + + +def save_received_role_grant( + userid: str, + registry_id_hex: str, + role_name: str, + bucket_id: str, + *, + eph_pk_b64: str, + ciphertext_b64: str, +) -> Path: + path = _received_grant_path(userid, registry_id_hex, role_name, bucket_id) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps({ + "registry_id_hex": registry_id_hex, + "role_name": role_name, + "bucket_id": bucket_id, + "eph_pk_b64": eph_pk_b64, + "ciphertext_b64": ciphertext_b64, + }, indent=2)) + try: + os.chmod(path, 0o600) + except OSError: + pass + return path + + +def list_received_role_grants(userid: str, registry_id_hex: str, role_name: str) -> list[str]: + base = state_dir(userid) / "received_grants" / registry_id_hex / role_name + if not base.is_dir(): + return [] + return sorted(p.stem for p in base.glob("*.json")) + + +def load_received_role_grant(userid: str, registry_id_hex: str, role_name: str, bucket_id: str) -> dict: + return json.loads(_received_grant_path(userid, registry_id_hex, role_name, bucket_id).read_text()) + + # ── local persistence (admin manifests) ────────────────────────────── def state_dir(userid: str) -> Path: diff --git a/demo/file_share_credentials.py b/demo/file_share_credentials.py index 5ec959d..cba15f8 100644 --- a/demo/file_share_credentials.py +++ b/demo/file_share_credentials.py @@ -29,6 +29,7 @@ from pathlib import Path import zkac from zkac.tcp import FramedSession, client_handshake_anon +import file_share_client as fsc import zkac_cli_adapter as cli @@ -106,6 +107,7 @@ def grant_role_p2p( role_name: str, recipient_contact_bundle: str, *, + bucket_role_grant: dict | None = None, connect_timeout_s: float = 8.0, ) -> dict: """Issue a BBS+ role credential and ship it to the recipient over an authenticated TCP session. @@ -148,6 +150,7 @@ def grant_role_p2p( "blind_sig_b64": _b64(bytes(blind_sig)), "member_secret_b64": _b64(bytes(req.member_secret())), "prover_blind_b64": _b64(bytes(req.prover_blind())), + "bucket_role_grant": bucket_role_grant or None, }).encode() rec_pk_bytes = bytes.fromhex(recipient_issuance_pk_hex) @@ -313,6 +316,20 @@ def listen_for_role_credential( raise RuntimeError("granted credential does not verify") _save_credential_json(userid, registry_id_hex, role_name, payload) + bucket_grant = payload.get("bucket_role_grant") + if isinstance(bucket_grant, dict): + bucket_id = bucket_grant.get("bucket_id") + eph_pk_b64 = bucket_grant.get("eph_pk_b64") + ciphertext_b64 = bucket_grant.get("ciphertext_b64") + if isinstance(bucket_id, str) and isinstance(eph_pk_b64, str) and isinstance(ciphertext_b64, str): + fsc.save_received_role_grant( + userid, + registry_id_hex, + role_name, + bucket_id, + eph_pk_b64=eph_pk_b64, + ciphertext_b64=ciphertext_b64, + ) framed.send(json.dumps({"ok": True, "status": "stored"}).encode()) return {"registry_id": registry_id_hex, "role": role_name} finally: diff --git a/demo/file_share_server.py b/demo/file_share_server.py index 20d016c..d58e5fb 100644 --- a/demo/file_share_server.py +++ b/demo/file_share_server.py @@ -8,7 +8,7 @@ Headless TCP service that combines: ``zkac-node registry create/get/update`` commands work unchanged. * A new file-share channel that, after BBS+ role authentication, exposes bucket primitives. The server only ever sees opaque ciphertext blobs and - per-recipient encrypted role grants; file names, contents and per-role + encrypted role grants; file names, contents and per-role visibility masks are never visible server-side. Run with:: @@ -23,12 +23,12 @@ from __future__ import annotations import argparse import base64 +import hashlib import json import os import socket import sys import threading -import traceback import uuid from pathlib import Path from typing import Any @@ -72,6 +72,11 @@ def _safe_id(value: str) -> str: return value +def _privacy_safe_error_tag(exc: BaseException) -> str: + """Return a coarse error label suitable for public service logs.""" + return type(exc).__name__ + + # ── opaque on-disk store ───────────────────────────────────────────── class _FileShareStore: @@ -81,11 +86,39 @@ class _FileShareStore: self._dir = data_dir self._reg_dir = data_dir / "registries" self._buckets_dir = data_dir / "buckets" + self._privacy_key: bytes = b"" for d in (self._dir, self._reg_dir, self._buckets_dir): d.mkdir(parents=True, exist_ok=True) _chmod(d, 0o700) self._lock = threading.Lock() + def set_privacy_key(self, key: bytes) -> None: + if not key: + raise RuntimeError("privacy key must not be empty") + self._privacy_key = bytes(key) + + def _require_privacy_key(self) -> bytes: + if not self._privacy_key: + raise RuntimeError("privacy key not initialized") + return self._privacy_key + + def _tag(self, kind: str, raw_id: str) -> str: + key = self._require_privacy_key() + raw = _safe_id(raw_id).encode("utf-8") + h = hashlib.sha256() + h.update(kind.encode("utf-8")) + h.update(b"\x00") + h.update(key) + h.update(b"\x00") + h.update(raw) + return h.hexdigest() + + def _registry_tag(self, registry_id_hex: str) -> str: + return self._tag("registry", registry_id_hex) + + def _role_tag(self, role_id_hex: str) -> str: + return self._tag("role", role_id_hex) + # transport key def load_or_create_keypair(self) -> zkac.Keypair: @@ -120,6 +153,14 @@ class _FileShareStore: print(f"[fs-server] skip registry {p.stem}: {exc}") return n + def list_registry_ids(self) -> list[str]: + out: list[str] = [] + for p in sorted(self._reg_dir.glob("*.state")): + cert = self._reg_dir / f"{p.stem}.cert" + if cert.exists(): + out.append(p.stem) + return out + # buckets def _bucket_dir(self, bucket_id: str) -> Path: @@ -128,6 +169,22 @@ class _FileShareStore: def _bucket_meta_path(self, bucket_id: str) -> Path: return self._bucket_dir(bucket_id) / "meta.json" + def _roles_index_path(self, registry_id_hex: str) -> Path: + return self._reg_dir / f"{_safe_id(registry_id_hex)}.roles.json" + + def _remember_role_id(self, registry_id_hex: str, role_id_hex: str) -> None: + p = self._roles_index_path(registry_id_hex) + roles: set[str] = set() + if p.is_file(): + try: + data = json.loads(p.read_text()) + if isinstance(data, list): + roles = {_safe_id(str(v)) for v in data} + except Exception: + roles = set() + roles.add(_safe_id(role_id_hex)) + _write_private_json(p, sorted(roles)) + def bucket_create(self, bucket_id: str, owner_registry_id_hex: str) -> None: bd = self._bucket_dir(bucket_id) with self._lock: @@ -138,17 +195,21 @@ class _FileShareStore: _chmod(bd, 0o700) _write_private_json(self._bucket_meta_path(bucket_id), { "bucket_id": bucket_id, - "owner_registry_id": owner_registry_id_hex, + "owner_registry_tag": self._registry_tag(owner_registry_id_hex), "finalized": False, - "role_acl": {}, # role_id_hex -> {"version": int, "allowed_blob_ids": [blob_id...]} + "role_acl": {}, # role_tag -> {"version": int, "allowed_blob_ids": [blob_id...]} }) def bucket_meta(self, bucket_id: str) -> dict: return json.loads(self._bucket_meta_path(bucket_id).read_text()) + def bucket_is_finalized(self, bucket_id: str) -> bool: + return bool(self.bucket_meta(bucket_id).get("finalized", False)) + def _bucket_require_owner(self, bucket_id: str, registry_id_hex: str) -> dict: meta = self.bucket_meta(bucket_id) - if meta.get("owner_registry_id") != registry_id_hex: + expected_owner_tag = self._registry_tag(registry_id_hex) + if meta.get("owner_registry_tag") != expected_owner_tag: raise RuntimeError("not bucket owner") return meta @@ -165,11 +226,18 @@ class _FileShareStore: for sub in ("blobs", "role_grants"): d = bd / sub if d.is_dir(): - for p in d.iterdir(): - try: - p.unlink() - except OSError: - pass + for p in d.rglob("*"): + if p.is_file(): + try: + p.unlink() + except OSError: + pass + for p in sorted(d.rglob("*"), reverse=True): + if p.is_dir(): + try: + p.rmdir() + except OSError: + pass try: d.rmdir() except OSError: @@ -194,29 +262,6 @@ class _FileShareStore: self._bucket_require_owner(bucket_id, registry_id_hex) (self._bucket_dir(bucket_id) / "blobs" / _safe_id(blob_id)).write_bytes(ciphertext) - def bucket_put_role_grant( - self, - bucket_id: str, - registry_id_hex: str, - recipient_pk_hex: str, - role_id_hex: str, - acl_version: int, - eph_pk_b64: str, - ciphertext_b64: str, - ) -> None: - with self._lock: - self._bucket_require_owner(bucket_id, registry_id_hex) - _safe_id(recipient_pk_hex) - payload = { - "bucket_id": bucket_id, - "role_id_hex": _safe_id(role_id_hex), - "acl_version": int(acl_version), - "eph_pk_b64": eph_pk_b64, - "ciphertext_b64": ciphertext_b64, - } - target = self._bucket_dir(bucket_id) / "role_grants" / f"{recipient_pk_hex}.json" - _write_private_json(target, payload) - def bucket_set_role_acl( self, bucket_id: str, @@ -227,7 +272,7 @@ class _FileShareStore: with self._lock: meta = self._bucket_require_owner(bucket_id, registry_id_hex) role_acl = dict(meta.get("role_acl", {})) - key = _safe_id(role_id_hex) + key = self._role_tag(role_id_hex) prev = role_acl.get(key, {}) prev_version = int(prev.get("version", 0)) if isinstance(prev, dict) else 0 role_acl[key] = { @@ -236,6 +281,7 @@ class _FileShareStore: } meta["role_acl"] = role_acl _write_private_json(self._bucket_meta_path(bucket_id), meta) + self._remember_role_id(registry_id_hex, role_id_hex) def bucket_get_blob(self, bucket_id: str, blob_id: str) -> bytes: return (self._bucket_dir(bucket_id) / "blobs" / _safe_id(blob_id)).read_bytes() @@ -243,7 +289,7 @@ class _FileShareStore: def bucket_blob_allowed_for_role(self, bucket_id: str, role_id_hex: str, blob_id: str) -> bool: meta = self.bucket_meta(bucket_id) role_acl = meta.get("role_acl", {}) - role_meta = role_acl.get(_safe_id(role_id_hex)) + role_meta = role_acl.get(self._role_tag(role_id_hex)) if not isinstance(role_meta, dict): return False allowed = role_meta.get("allowed_blob_ids") @@ -254,7 +300,7 @@ class _FileShareStore: def bucket_role_acl(self, bucket_id: str, role_id_hex: str) -> dict: meta = self.bucket_meta(bucket_id) role_acl = meta.get("role_acl", {}) - role_meta = role_acl.get(_safe_id(role_id_hex), {}) + role_meta = role_acl.get(self._role_tag(role_id_hex), {}) if not isinstance(role_meta, dict): return {"version": 0, "allowed_blob_ids": []} version = int(role_meta.get("version", 0)) @@ -266,20 +312,75 @@ class _FileShareStore: "allowed_blob_ids": [_safe_id(str(v)) for v in allowed], } - def bucket_get_role_grant(self, bucket_id: str, recipient_pk_hex: str) -> dict: - path = self._bucket_dir(bucket_id) / "role_grants" / f"{_safe_id(recipient_pk_hex)}.json" - return json.loads(path.read_text()) - - def buckets_for_recipient(self, recipient_pk_hex: str) -> list[str]: - """Bucket ids that have an encrypted role grant addressed to ``recipient_pk_hex``.""" - _safe_id(recipient_pk_hex) + def buckets_for_role_in_registry(self, role_id_hex: str, registry_id_hex: str) -> list[str]: + """Bucket ids where this authenticated role currently has non-empty ACL.""" + role_id_hex = _safe_id(role_id_hex) out: list[str] = [] for bd in sorted(self._buckets_dir.iterdir()): - grant = bd / "role_grants" / f"{recipient_pk_hex}.json" - if grant.is_file(): - out.append(bd.name) + bid = bd.name + try: + if not self.bucket_is_finalized(bid): + continue + if self.bucket_owner_registry_tag(bid) != self._registry_tag(registry_id_hex): + continue + acl = self.bucket_role_acl(bid, role_id_hex) + allowed = acl.get("allowed_blob_ids", []) + grants = self.bucket_get_role_grants(bid, role_id_hex) + current_acl_version = int(acl.get("version", -1)) + has_fresh_grant = any( + isinstance(g.get("acl_version"), int) and g["acl_version"] == current_acl_version + for g in grants + ) + if isinstance(allowed, list) and len(allowed) > 0 and has_fresh_grant: + out.append(bid) + except Exception: + continue return out + def bucket_put_role_grant( + self, + bucket_id: str, + registry_id_hex: str, + role_id_hex: str, + acl_version: int, + eph_pk_b64: str, + ciphertext_b64: str, + ) -> None: + with self._lock: + self._bucket_require_owner(bucket_id, registry_id_hex) + role_id_hex = _safe_id(role_id_hex) + role_tag = self._role_tag(role_id_hex) + payload = { + "bucket_id": bucket_id, + "role_tag": role_tag, + "acl_version": int(acl_version), + "eph_pk_b64": eph_pk_b64, + "ciphertext_b64": ciphertext_b64, + } + role_dir = self._bucket_dir(bucket_id) / "role_grants" / role_tag + role_dir.mkdir(parents=True, exist_ok=True) + target = role_dir / f"{uuid.uuid4().hex}.json" + _write_private_json(target, payload) + self._remember_role_id(registry_id_hex, role_id_hex) + + def bucket_get_role_grants(self, bucket_id: str, role_id_hex: str) -> list[dict]: + role_dir = self._bucket_dir(bucket_id) / "role_grants" / self._role_tag(role_id_hex) + if not role_dir.is_dir(): + return [] + out: list[dict] = [] + for p in sorted(role_dir.glob("*.json")): + try: + out.append(json.loads(p.read_text())) + except Exception: + continue + return out + + def bucket_owner_registry_tag(self, bucket_id: str) -> str: + owner = self.bucket_meta(bucket_id).get("owner_registry_tag") + if not isinstance(owner, str) or not owner: + raise RuntimeError("bucket metadata missing owner_registry_tag") + return owner + def buckets_owned_by(self, registry_id_hex: str) -> list[str]: out: list[str] = [] for bd in sorted(self._buckets_dir.iterdir()): @@ -287,12 +388,25 @@ class _FileShareStore: if not meta.is_file(): continue try: - if json.loads(meta.read_text()).get("owner_registry_id") == registry_id_hex: + owner_tag = self._registry_tag(registry_id_hex) + if json.loads(meta.read_text()).get("owner_registry_tag") == owner_tag: out.append(bd.name) except (OSError, json.JSONDecodeError): continue return out + def role_ids_for_registry(self, registry_id_hex: str) -> list[str]: + p = self._roles_index_path(registry_id_hex) + if not p.is_file(): + return [] + try: + data = json.loads(p.read_text()) + if not isinstance(data, list): + return [] + return sorted({_safe_id(str(v)) for v in data}) + except Exception: + return [] + # ── command dispatch (inside encrypted session) ────────────────────── @@ -378,13 +492,13 @@ def _dispatch_fs( store: _FileShareStore, ctx: dict, ) -> dict: - """File-share commands authenticated via ``ctx`` (registry_id, role_id, issuance_pk_hex).""" + """File-share commands authenticated via opaque per-session authorization context.""" try: action = cmd.get("cmd") registry_id_hex: str = ctx["registry_id_hex"] - role_id: bytes = ctx["role_id"] - issuance_pk_hex: str = ctx["issuance_pk_hex"] - is_admin = role_id == zkac.admin_role_id() + registry_tag: str = ctx["registry_tag"] + role_id_hex: str = ctx["role_id_hex"] + is_admin: bool = bool(ctx["is_admin"]) def _require_admin() -> None: if not is_admin: @@ -393,10 +507,8 @@ def _dispatch_fs( if action == "whoami": return { "ok": True, - "registry_id": registry_id_hex, - "role_id": role_id.hex(), "is_admin": is_admin, - "issuance_pk_hex": issuance_pk_hex, + "auth_scope": "admin" if is_admin else "credential", } if action == "bucket_create": @@ -413,19 +525,6 @@ def _dispatch_fs( store.bucket_put_blob(bid, registry_id_hex, blob_id, ciphertext) return {"ok": True} - if action == "bucket_put_role_grant": - _require_admin() - store.bucket_put_role_grant( - cmd["bucket_id"], - registry_id_hex, - cmd["recipient_pk_hex"], - cmd["role_id_hex"], - int(cmd["acl_version"]), - cmd["eph_pk_b64"], - cmd["ciphertext_b64"], - ) - return {"ok": True} - if action == "bucket_set_role_acl": _require_admin() store.bucket_set_role_acl( @@ -436,6 +535,18 @@ def _dispatch_fs( ) return {"ok": True} + if action == "bucket_put_role_grant": + _require_admin() + store.bucket_put_role_grant( + cmd["bucket_id"], + registry_id_hex, + cmd["role_id_hex"], + int(cmd["acl_version"]), + cmd["eph_pk_b64"], + cmd["ciphertext_b64"], + ) + return {"ok": True} + if action == "bucket_get_role_acl": _require_admin() role_acl = store.bucket_role_acl(cmd["bucket_id"], cmd["role_id_hex"]) @@ -456,31 +567,38 @@ def _dispatch_fs( return {"ok": True, "bucket_ids": store.buckets_owned_by(registry_id_hex)} if action == "fs_buckets": - return {"ok": True, "bucket_ids": store.buckets_for_recipient(issuance_pk_hex)} + return { + "ok": True, + "bucket_ids": store.buckets_for_role_in_registry( + role_id_hex, + registry_id_hex, + ), + } if action == "fs_get_role_grant": bid = cmd["bucket_id"] - grant = store.bucket_get_role_grant(bid, issuance_pk_hex) - if not is_admin: - expected_role = _safe_id(role_id.hex()) - granted_role_raw = grant.get("role_id_hex") - if not isinstance(granted_role_raw, str) or not granted_role_raw: - raise RuntimeError("permissions updated; role grant is outdated, request a fresh grant") - granted_role = _safe_id(granted_role_raw) - if granted_role != expected_role: - raise RuntimeError("role grant not valid for authenticated role") - current_acl = store.bucket_role_acl(bid, expected_role) - acl_version = grant.get("acl_version") - if not isinstance(acl_version, int): - raise RuntimeError("permissions updated; role grant is outdated, request a fresh grant") - if acl_version != int(current_acl.get("version", -2)): - raise RuntimeError("permissions updated; request a fresh role grant") - return {"ok": True, **grant} + if store.bucket_owner_registry_tag(bid) != registry_tag: + raise RuntimeError("bucket does not belong to authenticated registry") + if not store.bucket_is_finalized(bid): + raise RuntimeError("bucket is not finalized") + current_acl = store.bucket_role_acl(bid, role_id_hex) + acl_version = int(current_acl.get("version", -1)) + grants = [ + g for g in store.bucket_get_role_grants(bid, role_id_hex) + if isinstance(g.get("acl_version"), int) and g["acl_version"] == acl_version + ] + if not grants: + raise RuntimeError("permissions updated; request a fresh role grant") + return {"ok": True, "grants": grants} if action == "fs_get_blob": bid = cmd["bucket_id"] blob_id = cmd["blob_id"] - if not is_admin and not store.bucket_blob_allowed_for_role(bid, role_id.hex(), blob_id): + if store.bucket_owner_registry_tag(bid) != registry_tag: + raise RuntimeError("bucket does not belong to authenticated registry") + if not store.bucket_is_finalized(bid): + raise RuntimeError("bucket is not finalized") + if not is_admin and not store.bucket_blob_allowed_for_role(bid, role_id_hex, blob_id): raise RuntimeError("blob access denied by role mask") data = store.bucket_get_blob(bid, blob_id) return {"ok": True, "ciphertext_b64": _b64(data)} @@ -491,6 +609,61 @@ def _dispatch_fs( return {"error": str(exc)} +def _authenticate_fs_identity( + mgr: zkac.RegistryManager, + store: _FileShareStore, + proof: bytes, + transcript_hash: bytes, +) -> dict[str, object] | None: + """Resolve opaque auth context from proof without client-supplied identifiers.""" + registry_ids = store.list_registry_ids() + admin_matches: list[str] = [] + role_matches: list[tuple[str, str]] = [] + for rid_hex in registry_ids: + try: + rid = bytes.fromhex(rid_hex) + except ValueError: + continue + try: + if mgr.verify_admin(rid, proof, transcript_hash): + admin_matches.append(rid_hex) + except Exception: + pass + for role_id_hex in store.role_ids_for_registry(rid_hex): + try: + role_id = bytes.fromhex(role_id_hex) + except ValueError: + continue + if role_id == zkac.admin_role_id(): + continue + try: + if mgr.verify_presentation(rid, role_id, proof, transcript_hash): + role_matches.append((rid_hex, role_id_hex)) + except Exception: + continue + if len(admin_matches) == 1 and not role_matches: + rid_hex = admin_matches[0] + role_hex = zkac.admin_role_id().hex() + return { + "registry_id_hex": rid_hex, + "registry_tag": store._registry_tag(rid_hex), + "role_id_hex": role_hex, + "role_tag": store._role_tag(role_hex), + "is_admin": True, + } + if not admin_matches and len(role_matches) == 1: + rid_hex, role_hex = role_matches[0] + return { + "registry_id_hex": rid_hex, + "registry_tag": store._registry_tag(rid_hex), + "role_id_hex": role_hex, + "role_tag": store._role_tag(role_hex), + "is_admin": False, + } + # Ambiguous or invalid proof. + return None + + # ── per-connection handler ──────────────────────────────────────────── def _handle_conn( @@ -503,7 +676,6 @@ def _handle_conn( idle_timeout_s: float, slots: threading.BoundedSemaphore, ) -> None: - peer = f"{addr[0]}:{addr[1]}" try: conn.settimeout(idle_timeout_s) session = server_handshake_anon(conn, node) @@ -525,27 +697,16 @@ def _handle_conn( if op == "fs": try: - registry_id = bytes.fromhex(hello["registry_id"]) - role_id = bytes.fromhex(hello["role_id"]) proof = _unb64(hello["bbs_auth_b64"]) - issuance_pk_hex = hello["issuance_pk_hex"] - _safe_id(issuance_pk_hex) except (KeyError, ValueError) as exc: framed.send(json.dumps({"error": f"invalid fs hello: {exc}"}).encode()) return - if role_id == zkac.admin_role_id(): - ok = mgr.verify_admin(registry_id, proof, transcript_hash) - else: - ok = mgr.verify_presentation(registry_id, role_id, proof, transcript_hash) - if not ok: + auth = _authenticate_fs_identity(mgr, store, proof, transcript_hash) + if auth is None: framed.send(json.dumps({"error": "auth failed"}).encode()) return framed.send(json.dumps({"ok": True, "status": "authenticated"}).encode()) - ctx = { - "registry_id_hex": registry_id.hex(), - "role_id": role_id, - "issuance_pk_hex": issuance_pk_hex, - } + ctx = auth while True: try: cmd = json.loads(framed.recv()) @@ -560,8 +721,8 @@ def _handle_conn( except (ConnectionError, BrokenPipeError, OSError): pass except Exception as exc: - print(f"[fs-server] {peer} error: {exc}") - traceback.print_exc() + # Privacy model: never emit client endpoint or request-linked payloads. + print(f"[fs-server] connection error ({_privacy_safe_error_tag(exc)})") finally: conn.close() slots.release() @@ -582,6 +743,7 @@ def serve( data_dir.mkdir(parents=True, exist_ok=True) store = _FileShareStore(data_dir) kp = store.load_or_create_keypair() + store.set_privacy_key(bytes(kp.secret_key_bytes())) server_pk_b64 = _b64(kp.public_key().to_bytes()) pk_hex = kp.public_key().to_bytes().hex() node = zkac.Node(kp) diff --git a/demo/file_share_smoke.py b/demo/file_share_smoke.py index 1f4ba86..4ff4b8b 100644 --- a/demo/file_share_smoke.py +++ b/demo/file_share_smoke.py @@ -7,7 +7,7 @@ Exercises: * admin (Alice) creates a registry on the file-share server, * admin uploads a folder as an encrypted bucket with per-role visibility masks, * Alice issues a ZKAC role credential to Bob via direct P2P grant, -* Alice pushes a per-recipient bucket role grant to Bob, +* Alice uploads an anonymous role-grant envelope to the server, * Bob authenticates to the file-share server with his role credential, downloads, and decrypts the files his role can see, * server opacity: every byte at rest in the bucket directory is ciphertext @@ -108,7 +108,6 @@ def _open_admin_session(userid: str, server: str, server_pk_hex: str, registry_i registry_id_hex=registry_id, role_id=zkac.admin_role_id(), credential=cred, - user_issuance_pk_hex=secrets["issuance_pk_hex"], ) @@ -122,7 +121,6 @@ def _open_role_session(userid: str, server: str, server_pk_hex: str, registry_id registry_id_hex=registry_id, role_id=zkac.role_id(role_name), credential=cred, - user_issuance_pk_hex=secrets["issuance_pk_hex"], ) @@ -182,8 +180,33 @@ def main() -> int: raise RuntimeError("bob listener did not start") bob_contact = cli.show_user_contact("bob", peer=f"127.0.0.1:{listener_port}") print(f"[smoke] bob listening on 127.0.0.1:{listener_port}") + # --- alice uploads bucket + sets masks -------------------------------- + files_in_order = fsc.flatten_folder(src) + rel_paths = [str(p.relative_to(src.resolve())) for p in files_in_order] + print(f"[smoke] flattened: {rel_paths}") + # mask: viewer sees only first file (alpha.txt), editor sees everything. + viewer_mask = "1" + "0" * (len(files_in_order) - 1) + editor_mask = "1" * len(files_in_order) + bob_secrets = cli.load_identity_secrets("bob") + + with _open_admin_session("alice", server_addr, server_pk_hex, rid) as sess: + manifest = fsc.upload_bucket( + sess, src, server=server_addr, registry_id_hex=rid, + ) + manifest.role_masks = {"viewer": viewer_mask, "editor": editor_mask} + fsc.apply_role_masks_to_server(sess, manifest) + fsc.push_role_grant(sess, manifest, "viewer", bob_secrets["issuance_pk_hex"]) + fsc.save_manifest("alice", manifest) + print(f"[smoke] uploaded bucket {manifest.bucket_id} with role ACLs + anonymous grant envelope") + try: - fscred.grant_role_p2p("alice", server_addr, rid, "viewer", bob_contact) + fscred.grant_role_p2p( + "alice", + server_addr, + rid, + "viewer", + bob_contact, + ) except RuntimeError as exc: listener.stop() print(f"[smoke] grant failed: {exc}\n[smoke] bob listener output:\n{listener.output()}") @@ -196,25 +219,6 @@ def main() -> int: assert received and received["role"] == "viewer" assert received["registry_id"] == rid - # --- alice uploads bucket + sets masks -------------------------------- - files_in_order = fsc.flatten_folder(src) - rel_paths = [str(p.relative_to(src.resolve())) for p in files_in_order] - print(f"[smoke] flattened: {rel_paths}") - # mask: viewer sees only first file (alpha.txt), editor sees everything. - viewer_mask = "1" + "0" * (len(files_in_order) - 1) - editor_mask = "1" * len(files_in_order) - - with _open_admin_session("alice", server_addr, server_pk_hex, rid) as sess: - manifest = fsc.upload_bucket( - sess, src, server=server_addr, registry_id_hex=rid, - ) - manifest.role_masks = {"viewer": viewer_mask, "editor": editor_mask} - # bob's issuance pk -> push viewer role grant - bob_secrets = cli.load_identity_secrets("bob") - fsc.push_role_grant(sess, manifest, "viewer", bob_secrets["issuance_pk_hex"]) - fsc.save_manifest("alice", manifest) - print(f"[smoke] uploaded bucket {manifest.bucket_id} with role grants") - # --- bob downloads using his viewer credential ------------------------ with _open_role_session("bob", server_addr, server_pk_hex, rid, "viewer") as sess: accessible = sess.fs_buckets() diff --git a/demo/file_share_tui.py b/demo/file_share_tui.py index 4db6be9..557459b 100644 --- a/demo/file_share_tui.py +++ b/demo/file_share_tui.py @@ -352,7 +352,6 @@ class FileShareApp(App[None]): registry_id_hex=registry_id_hex, role_id=role_id, credential=credential, - user_issuance_pk_hex=ident.issuance_pk_hex, ) async def _choose_index(self, title: str, options: list[str], default: int = 1) -> int: @@ -713,7 +712,7 @@ class FileShareApp(App[None]): fsc.push_role_grant(sess, manifest, role, recipient_pk_hex) fsc.save_manifest(ident.userid, manifest) self.current_bucket_id = manifest.bucket_id - self.write_log(f"[share] pushed bucket grant to {recipient_pk_hex[:16]}...") + self.write_log("[share] uploaded anonymous role-grant envelope (no recipient identifier on server)") async def menu_listen(self) -> None: ident = self._require_user() @@ -781,8 +780,9 @@ class FileShareApp(App[None]): with self._open_session(cred["registry_id"], cred["role"]) as sess: who = sess.whoami() self.write_log( - f"[inbox] authenticated registry={who['registry_id'][:16]}... " - f"role_id={who['role_id'][:16]}..." + "[inbox] authenticated " + f"scope={who.get('auth_scope', 'unknown')} " + f"admin={bool(who.get('is_admin', False))}" ) if cred["role"] == "__admin__": bids = sess.bucket_list_owned() diff --git a/demo/fs_data/registries/519bc59c917f122245d5a6a131ec42f5e3b57ae0b73c8b99a4e0cafc9c3eedab.state b/demo/fs_data/registries/519bc59c917f122245d5a6a131ec42f5e3b57ae0b73c8b99a4e0cafc9c3eedab.state deleted file mode 100644 index 1a99c493a6b24f1b4617648cc53784357ebefdf6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 628 zcmWHXeRR&mdLbp(tIHM|zH$2ccx%;z?Ka&rmpnN2XO7+5)r<_pfdrtk)`+K)J<;J; zMS40<`t_QG%#UGv-BF!C<4W$H9?!2SrMF|v77J~AW^5QExZguJu9`XQ^S52mRkF#i zX2#m=zk4lPT=!$w6Mn}neBZ^^)cjuaXAyVD*>-KeGg0Mzk~*JsR^2=@eM7+7mD!BV<@~{2H1%j$w0-R3LI~RIOn@)WA;PiTSt4hZm)jh;p?o_BJoaoW&fuM mr%v2tU_NbmdxzWez4C{3x%v+-zyCq!vX%a~yqlZp=@tOQZ4mYV diff --git a/demo/fs_data/registries/a0e5ed90dafc9ddfa18197b599a63ea5bc43228a3cf1d5f64a043994951c31ea.cert b/demo/fs_data/registries/a0e5ed90dafc9ddfa18197b599a63ea5bc43228a3cf1d5f64a043994951c31ea.cert deleted file mode 100644 index 0820bfd0e3f9fcfde8cd0c68af0775e64fe09623..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 336 zcmV-W0k8gzdY{x{Xod!7C!k79z?x5)wX5oY@J#lyc0ag8j}3REW%4I^?ix=t(W<@ifshMx zgI(?!Iy>-P^)mPWAs7|#d4A->pDAKnk34hw(m1_5C$V7J1>@2sQyS-Axzd*Z)oH;* zpZR5fY_^5Itm#^&-o5J~-?No}>B{#UFBJ=_Tkv+lD`SZAzoN=hu2G5?|UJL84-x8S{(Kc)7$BwJpq6WUErx2%1q m`&4u?TamF$Cy$HG<0mbhT59dJQ?@5fnlRVs>#WoCbPE8B0Tq$} diff --git a/demo/fs_data/registries/fc2c7a6c4a57247946f5bcf006fb92c0ad7469322dcef8269bc8694ecf262386.cert b/demo/fs_data/registries/fc2c7a6c4a57247946f5bcf006fb92c0ad7469322dcef8269bc8694ecf262386.cert deleted file mode 100644 index 562c77928c7dba57e76d723011ca7f8c666f0422..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 336 zcmV-W0k8gzp-r+kZLzps#0MpiC$Mms>#IK(HZ;HS&gQ$5=;`>D>qUp4_YQ}?DdkA>B@#g+U8aR=^$ zHSRpx{)ArxA8g^g>&yod8%M{vb;@zEU0vkHmTuHKpiLM$`- z$n4FaxpLq|l4DmI#Mb_(w;e?WNzpeMU*zIsA8>_%LS#oTvI#j1Zt|!X5XeE(%aUPd zi>;qZ%*1{GR-2(c@|7s404dc}rZHA^ifAQUa5CPeTNSw63m0zkh2|8}i#D|zSi_`C i(|a%j10tq1fa4dK23+^PR{ZU zNebj+%E{lO>$rcSjakWy<^@ew8FOa7SH1QB%C05c6MLdRieFoIt(wn$cW>!Er>ksJ z{BFJOsoS-B{yW?BlT}$d)}IgVlXuqo{w(NA{oD;@9Y4D-y|c;gmz=**Jf=)0zv*eq zjh6Vxy=Dv(7rnVR^~0exoYAwlRFyCR-LSjf@X)OL*4BdNub6XE8ZC7vzAuk{f8eoA z8^_XE)kh-qa3?Ix{snxke7yQ_u8&J|uJotXT5E!h44F49WENm<$?()`vY@A10INvX AIsgCw diff --git a/demo/test_demo_privacy_guardrails.py b/demo/test_demo_privacy_guardrails.py new file mode 100644 index 0000000..20f65ce --- /dev/null +++ b/demo/test_demo_privacy_guardrails.py @@ -0,0 +1,192 @@ +import json +import sys +import threading +from pathlib import Path + +import zkac + +DEMO_DIR = Path(__file__).resolve().parent +if str(DEMO_DIR) not in sys.path: + sys.path.insert(0, str(DEMO_DIR)) + +import file_share_client as fsc # noqa: E402 +import file_share_server as fss # noqa: E402 + + +def _make_credential() -> tuple[bytes, zkac.Credential]: + issuer = zkac.BbsIssuer() + pk = issuer.public_key() + role_id = zkac.role_id("viewer") + req = zkac.prepare_blind_request() + blind_sig = issuer.issue_blind(req.commitment_with_proof(), role_id, 1) + cred = zkac.Credential.finalize( + blind_sig, + req.member_secret(), + req.prover_blind(), + role_id, + 1, + pk, + ) + return role_id, cred + + +def test_open_session_fs_hello_contains_only_proof(monkeypatch): + role_id, credential = _make_credential() + sent_payloads: list[dict] = [] + + class _FakeSocket: + def settimeout(self, _value): + return None + + def close(self): + return None + + class _FakeHandshakeSession: + def transcript_hash(self) -> bytes: + return b"\x11" * 32 + + class _FakeFramedSession: + def __init__(self, _sock, _session): + pass + + def send(self, payload: bytes) -> None: + sent_payloads.append(json.loads(payload.decode("utf-8"))) + + def recv(self) -> bytes: + return b'{"ok": true, "status": "authenticated"}' + + monkeypatch.setattr(fsc.socket, "create_connection", lambda *_args, **_kwargs: _FakeSocket()) + monkeypatch.setattr(fsc, "client_handshake_anon", lambda *_args, **_kwargs: _FakeHandshakeSession()) + monkeypatch.setattr(fsc, "FramedSession", _FakeFramedSession) + + sess = fsc.open_session( + "127.0.0.1:9879", + server_pk_hex=zkac.Keypair().public_key().to_bytes().hex(), + user_transport_secret=bytes(zkac.Keypair().secret_key_bytes()), + registry_id_hex="00" * 32, + role_id=role_id, + credential=credential, + ) + sess.close() + + assert len(sent_payloads) == 1 + hello = sent_payloads[0] + assert set(hello.keys()) == {"op", "bbs_auth_b64"} + assert hello["op"] == "fs" + assert isinstance(hello["bbs_auth_b64"], str) and hello["bbs_auth_b64"] + + +def test_handle_conn_error_log_never_includes_peer_endpoint(monkeypatch, tmp_path, capsys): + class _DummyConn: + def settimeout(self, _value): + return None + + def close(self): + return None + + def _raise_handshake(_conn, _node): + raise RuntimeError("boom") + + monkeypatch.setattr(fss, "server_handshake_anon", _raise_handshake) + + slots = threading.BoundedSemaphore(1) + assert slots.acquire(blocking=False) + + fss._handle_conn( + _DummyConn(), + ("203.0.113.44", 4242), + zkac.Node(zkac.Keypair()), + zkac.RegistryManager(), + fss._FileShareStore(tmp_path), + "", + 5.0, + slots, + ) + + out = capsys.readouterr().out + assert "[fs-server] connection error (RuntimeError)" in out + assert "203.0.113.44" not in out + assert ":4242" not in out + + +def test_bucket_metadata_uses_opaque_tags(tmp_path): + store = fss._FileShareStore(tmp_path) + store.set_privacy_key(b"privacy-key-32-bytes-minimum-seed") + registry_id = "aa" * 32 + role_id = "bb" * 32 + bucket_id = "cc" * 16 + blob_id = "dd" * 16 + + store.bucket_create(bucket_id, registry_id) + meta = store.bucket_meta(bucket_id) + assert "owner_registry_id" not in meta + assert isinstance(meta.get("owner_registry_tag"), str) + assert len(meta["owner_registry_tag"]) == 64 + + store.bucket_set_role_acl(bucket_id, registry_id, role_id, [blob_id]) + meta2 = store.bucket_meta(bucket_id) + acl_keys = list(meta2.get("role_acl", {}).keys()) + assert acl_keys and acl_keys[0] != role_id + assert all(len(k) == 64 for k in acl_keys) + + store.bucket_put_role_grant(bucket_id, registry_id, role_id, 1, "eph", "ct") + grants_root = tmp_path / "buckets" / bucket_id / "role_grants" + assert (grants_root / role_id).exists() is False + role_dirs = [p.name for p in grants_root.iterdir() if p.is_dir()] + assert role_dirs and all(len(d) == 64 for d in role_dirs) + + +def test_auth_scan_does_not_return_early(): + class _Mgr: + def __init__(self): + self.admin_checks: list[str] = [] + self.role_checks: list[tuple[str, str]] = [] + + def verify_admin(self, rid: bytes, _proof: bytes, _th: bytes) -> bool: + self.admin_checks.append(rid.hex()) + return rid.hex() == ("11" * 32) + + def verify_presentation(self, rid: bytes, role_id: bytes, _proof: bytes, _th: bytes) -> bool: + self.role_checks.append((rid.hex(), role_id.hex())) + return False + + class _Store: + def list_registry_ids(self): + return ["11" * 32, "22" * 32] + + def role_ids_for_registry(self, rid_hex: str): + if rid_hex == "11" * 32: + return ["33" * 32] + return ["44" * 32] + + def _registry_tag(self, rid_hex: str): + return "r-" + rid_hex[:8] + + def _role_tag(self, role_hex: str): + return "k-" + role_hex[:8] + + mgr = _Mgr() + auth = fss._authenticate_fs_identity(mgr, _Store(), b"proof", b"nonce") + assert auth is not None and auth["is_admin"] is True + # Both registries must be checked even though first one matched admin. + assert mgr.admin_checks == ["11" * 32, "22" * 32] + # Role checks also run for all known role candidates. + assert mgr.role_checks == [("11" * 32, "33" * 32), ("22" * 32, "44" * 32)] + + +def test_dispatch_whoami_does_not_expose_registry_or_role(): + resp = fss._dispatch_fs( + {"cmd": "whoami"}, + store=None, # not used by whoami branch + ctx={ + "registry_id_hex": "11" * 32, + "registry_tag": "r-tag", + "role_id_hex": "22" * 32, + "role_tag": "k-tag", + "is_admin": False, + }, + ) + assert resp["ok"] is True + assert "registry_id" not in resp + assert "role_id" not in resp + assert resp["auth_scope"] == "credential" From 259e904c643634f9b54d2102b1ce43eb8225e693 Mon Sep 17 00:00:00 2001 From: everbarry Date: Thu, 7 May 2026 22:34:41 +0200 Subject: [PATCH 3/4] up gitignore --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index e9b3e99..3f699ac 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,10 @@ cli/zkac_cli/__pycache__ # wasm-pack output for browser demo demo/static/pkg/ + +# file-share demo runtime / local-only artifacts +demo/__pycache__/ +demo/fs_data/ +demo/.demo_state/ +demo/creds/ +demo/test_bucket/ From 6251a039ee438e55ff0dfdc7daf7170fd0665549 Mon Sep 17 00:00:00 2001 From: everbarry Date: Thu, 7 May 2026 22:46:07 +0200 Subject: [PATCH 4/4] cleanup --- .github/workflows/fuzz-smoke.yml | 25 ------------------------- .gitignore | 3 +++ 2 files changed, 3 insertions(+), 25 deletions(-) delete mode 100644 .github/workflows/fuzz-smoke.yml diff --git a/.github/workflows/fuzz-smoke.yml b/.github/workflows/fuzz-smoke.yml deleted file mode 100644 index 34e0ddc..0000000 --- a/.github/workflows/fuzz-smoke.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: fuzz-smoke - -on: - push: - branches: [master, main] - pull_request: - branches: [master, main] - -jobs: - libfuzzer: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - name: Install cargo-fuzz - uses: taiki-e/install-action@cargo-fuzz - - name: Build fuzz targets (no sanitizer — stable) - run: cargo fuzz build -s none - working-directory: ${{ github.workspace }} - - name: Smoke all libFuzzer targets - run: bash scripts/fuzz-libfuzzer.sh - working-directory: ${{ github.workspace }} - env: - FUZZ_RUNS: "2000" diff --git a/.gitignore b/.gitignore index 3f699ac..66ab9fc 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ demo/fs_data/ demo/.demo_state/ demo/creds/ demo/test_bucket/ + +# Workflow kept locally only (not versioned) +.github/workflows/fuzz-smoke.yml