paramant-sdk 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: paramant-sdk
3
+ Version: 1.0.0
4
+ Summary: PARAMANT Ghost Pipe SDK — post-quantum burn-on-read secure transport
5
+ Author-email: PARAMANT <hello@paramant.app>
6
+ License: BUSL-1.1
7
+ Project-URL: Homepage, https://paramant.app
8
+ Project-URL: Documentation, https://paramant.app/docs
9
+ Project-URL: Repository, https://github.com/paramant/sdk-py
10
+ Keywords: paramant,ghost-pipe,post-quantum,ml-kem,burn-on-read,secure-messaging,e2ee
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Security :: Cryptography
19
+ Classifier: Topic :: Communications
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: cryptography>=42.0
23
+ Provides-Extra: mlkem
24
+ Requires-Dist: pqcrypto; extra == "mlkem"
25
+
26
+ # paramant-sdk
27
+
28
+ PARAMANT Ghost Pipe SDK for Python. Post-quantum burn-on-read secure transport.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install paramant-sdk
34
+ ```
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ from paramant_sdk import GhostPipe
40
+
41
+ # Sender
42
+ sender = GhostPipe(api_key='pk_live_...', device='sender-001')
43
+ hash_ = sender.send(b'confidential payload')
44
+ print('hash:', hash_)
45
+
46
+ # Receiver
47
+ receiver = GhostPipe(api_key='pk_live_...', device='receiver-001')
48
+ data = receiver.receive(hash_)
49
+ print('received:', data)
50
+ # blob is burned after receive
51
+ ```
52
+
53
+ ## API
54
+
55
+ ```python
56
+ gp = GhostPipe(api_key, device, relay='', sector='health')
57
+
58
+ hash_ = gp.send(data: bytes, ttl=300) -> str
59
+ data = gp.receive(hash_: str) -> bytes
60
+ status = gp.status(hash_: str) -> dict
61
+ entries = gp.audit(limit=100) -> list
62
+ info = gp.health() -> dict
63
+ gp.listen(on_receive: callable, interval=3)
64
+ ```
65
+
66
+ ### Multi-relay cluster
67
+
68
+ ```python
69
+ from paramant_sdk import GhostPipeCluster
70
+
71
+ cluster = GhostPipeCluster(
72
+ api_key='pk_live_...',
73
+ device='device-001',
74
+ relays=['https://health.paramant.app', 'https://relay.paramant.app'],
75
+ )
76
+ hash_ = cluster.send(data)
77
+ ```
78
+
79
+ ## Protocol
80
+
81
+ - **Encryption**: ML-KEM-768 + ECDH P-256 + AES-256-GCM (hybrid PQC)
82
+ - **Burn-on-read**: blob deleted after first `receive()`
83
+ - **Sectors**: EU/DE Hetzner + Fly.io anycast
84
+
85
+ ## License
86
+
87
+ BUSL-1.1 — [paramant.app](https://paramant.app)
@@ -0,0 +1,62 @@
1
+ # paramant-sdk
2
+
3
+ PARAMANT Ghost Pipe SDK for Python. Post-quantum burn-on-read secure transport.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install paramant-sdk
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ from paramant_sdk import GhostPipe
15
+
16
+ # Sender
17
+ sender = GhostPipe(api_key='pk_live_...', device='sender-001')
18
+ hash_ = sender.send(b'confidential payload')
19
+ print('hash:', hash_)
20
+
21
+ # Receiver
22
+ receiver = GhostPipe(api_key='pk_live_...', device='receiver-001')
23
+ data = receiver.receive(hash_)
24
+ print('received:', data)
25
+ # blob is burned after receive
26
+ ```
27
+
28
+ ## API
29
+
30
+ ```python
31
+ gp = GhostPipe(api_key, device, relay='', sector='health')
32
+
33
+ hash_ = gp.send(data: bytes, ttl=300) -> str
34
+ data = gp.receive(hash_: str) -> bytes
35
+ status = gp.status(hash_: str) -> dict
36
+ entries = gp.audit(limit=100) -> list
37
+ info = gp.health() -> dict
38
+ gp.listen(on_receive: callable, interval=3)
39
+ ```
40
+
41
+ ### Multi-relay cluster
42
+
43
+ ```python
44
+ from paramant_sdk import GhostPipeCluster
45
+
46
+ cluster = GhostPipeCluster(
47
+ api_key='pk_live_...',
48
+ device='device-001',
49
+ relays=['https://health.paramant.app', 'https://relay.paramant.app'],
50
+ )
51
+ hash_ = cluster.send(data)
52
+ ```
53
+
54
+ ## Protocol
55
+
56
+ - **Encryption**: ML-KEM-768 + ECDH P-256 + AES-256-GCM (hybrid PQC)
57
+ - **Burn-on-read**: blob deleted after first `receive()`
58
+ - **Sectors**: EU/DE Hetzner + Fly.io anycast
59
+
60
+ ## License
61
+
62
+ BUSL-1.1 — [paramant.app](https://paramant.app)
@@ -0,0 +1,545 @@
1
+ """
2
+ PARAMANT Ghost Pipe SDK v1.0.0
3
+ Python SDK voor quantum-safe datatransport
4
+
5
+ pip install cryptography
6
+
7
+ Gebruik:
8
+ from paramant import GhostPipe
9
+
10
+ # Zender
11
+ gp = GhostPipe(api_key='pgp_xxx', device='mri-001')
12
+ hash = gp.send(open('scan.dcm','rb').read())
13
+ print(f'Hash voor ontvanger: {hash}')
14
+
15
+ # Ontvanger
16
+ gp = GhostPipe(api_key='pgp_xxx', device='mri-001')
17
+ gp.listen(on_receive=lambda data, meta: save(data))
18
+ """
19
+ import base64, hashlib, json, os, struct, time
20
+ import urllib.request, urllib.error
21
+ from typing import Callable, Optional
22
+
23
+ __version__ = '1.0.0'
24
+
25
+ SECTOR_RELAYS = {
26
+ 'health': 'https://health.paramant.app',
27
+ 'iot': 'https://iot.paramant.app',
28
+ 'legal': 'https://legal.paramant.app',
29
+ 'finance': 'https://finance.paramant.app',
30
+ 'relay': 'https://relay.paramant.app',
31
+ }
32
+ BLOCK = 5 * 1024 * 1024
33
+ UA = f'paramant-sdk/{__version__}'
34
+
35
+
36
+ class GhostPipeError(Exception):
37
+ pass
38
+
39
+
40
+ class GhostPipe:
41
+ """
42
+ PARAMANT Ghost Pipe client.
43
+
44
+ Quantum-safe end-to-end encrypted datatransport.
45
+ Relay ziet NOOIT plaintext. Burn-on-read na ophalen.
46
+
47
+ Args:
48
+ api_key: pgp_... API key van paramant.app/dashboard
49
+ device: Uniek apparaat-ID (zender en ontvanger gebruiken hetzelfde)
50
+ relay: Relay URL (automatisch gedetecteerd op basis van key)
51
+ secret: Extra geheim voor encryptie (optioneel, default=api_key)
52
+ """
53
+
54
+ def __init__(self, api_key: str, device: str, relay: str = '', secret: str = ''):
55
+ if not api_key.startswith('pgp_'):
56
+ raise GhostPipeError('API key moet beginnen met pgp_')
57
+ self.api_key = api_key
58
+ self.device = device
59
+ self.secret = secret or api_key
60
+ self.relay = relay or self._detect_relay()
61
+ if not self.relay:
62
+ raise GhostPipeError('Geen relay bereikbaar. Controleer API key.')
63
+ self._keypair = None
64
+
65
+ # ── Relay detectie ────────────────────────────────────────────────────────
66
+ def _detect_relay(self) -> Optional[str]:
67
+ for relay in SECTOR_RELAYS.values():
68
+ try:
69
+ r = urllib.request.urlopen(
70
+ urllib.request.Request(f'{relay}/v2/check-key?k={self.api_key}',
71
+ headers={'User-Agent': UA}), timeout=4)
72
+ if json.loads(r.read()).get('valid'):
73
+ return relay
74
+ except Exception:
75
+ pass
76
+ return None
77
+
78
+ # ── HTTP helpers ──────────────────────────────────────────────────────────
79
+ def _get(self, path: str, params: dict = None):
80
+ import urllib.parse
81
+ url = self.relay + path
82
+ if params: url += '?' + urllib.parse.urlencode(params)
83
+ req = urllib.request.Request(url, headers={'User-Agent': UA, 'X-Api-Key': self.api_key})
84
+ try:
85
+ with urllib.request.urlopen(req, timeout=30) as r:
86
+ return r.status, r.read()
87
+ except urllib.error.HTTPError as e:
88
+ return e.code, e.read()
89
+
90
+ def _post(self, path: str, body: bytes, content_type: str = 'application/json'):
91
+ req = urllib.request.Request(
92
+ self.relay + path, data=body, method='POST',
93
+ headers={'Content-Type': content_type, 'X-Api-Key': self.api_key, 'User-Agent': UA})
94
+ try:
95
+ with urllib.request.urlopen(req, timeout=30) as r:
96
+ return r.status, r.read()
97
+ except urllib.error.HTTPError as e:
98
+ return e.code, e.read()
99
+
100
+ # ── Crypto ────────────────────────────────────────────────────────────────
101
+ def _get_crypto(self):
102
+ try:
103
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
104
+ from cryptography.hazmat.primitives import hashes
105
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
106
+ from cryptography.hazmat.primitives.asymmetric.ec import (
107
+ generate_private_key, ECDH, SECP256R1)
108
+ from cryptography.hazmat.primitives.serialization import (
109
+ Encoding, PublicFormat, PrivateFormat, NoEncryption,
110
+ load_der_public_key, load_der_private_key)
111
+ from cryptography.hazmat.backends import default_backend
112
+ return dict(HKDF=HKDF, hsh=hashes, AES=AESGCM, gen=generate_private_key,
113
+ ECDH=ECDH, curve=SECP256R1, Enc=Encoding, Pub=PublicFormat,
114
+ Priv=PrivateFormat, NoEnc=NoEncryption,
115
+ lpub=load_der_public_key, lpriv=load_der_private_key,
116
+ be=default_backend)
117
+ except ImportError:
118
+ raise GhostPipeError('pip install cryptography')
119
+
120
+ def _try_kyber(self):
121
+ try:
122
+ from kyber import Kyber768
123
+ return Kyber768
124
+ except ImportError:
125
+ return None
126
+
127
+ def _load_keypair(self):
128
+ """Laad of genereer keypair voor dit device."""
129
+ if self._keypair:
130
+ return self._keypair
131
+ state_dir = os.path.expanduser('~/.paramant')
132
+ path = os.path.join(state_dir, self.device.replace('/','_') + '.keypair.json')
133
+ if os.path.exists(path):
134
+ self._keypair = json.load(open(path))
135
+ return self._keypair
136
+ c = self._get_crypto(); K = self._try_kyber(); be = c['be']()
137
+ os.makedirs(state_dir, exist_ok=True)
138
+ priv = c['gen'](c['curve'](), be)
139
+ pub = priv.public_key()
140
+ pd = priv.private_bytes(c['Enc'].DER, c['Priv'].PKCS8, c['NoEnc']())
141
+ pubd = pub.public_bytes(c['Enc'].DER, c['Pub'].SubjectPublicKeyInfo)
142
+ kpub = b''; kpriv = b''
143
+ if K:
144
+ kpub, kpriv = K.keygen()
145
+ kp = {'device': self.device, 'ecdh_priv': pd.hex(), 'ecdh_pub': pubd.hex(),
146
+ 'kyber_pub': kpub.hex() if kpub else '', 'kyber_priv': kpriv.hex() if kpriv else ''}
147
+ with open(path, 'w') as f: json.dump(kp, f)
148
+ os.chmod(path, 0o600)
149
+ self._keypair = kp
150
+ return kp
151
+
152
+ def _register_pubkeys(self):
153
+ """Registreer pubkeys bij relay."""
154
+ kp = self._load_keypair()
155
+ body = json.dumps({'device_id': self.device, 'ecdh_pub': kp['ecdh_pub'],
156
+ 'kyber_pub': kp.get('kyber_pub', '')}).encode()
157
+ status, resp = self._post('/v2/pubkey', body)
158
+ if status != 200:
159
+ raise GhostPipeError(f'Pubkey registratie mislukt: {resp.decode()[:100]}')
160
+
161
+ def _fetch_receiver_pubkeys(self):
162
+ """Haal pubkeys op van relay (voor encryptie)."""
163
+ status, body = self._get(f'/v2/pubkey/{self.device}')
164
+ if status == 404:
165
+ raise GhostPipeError('Geen pubkeys voor dit device. Start ontvanger eerst met receive_setup().')
166
+ if status != 200:
167
+ raise GhostPipeError(f'Pubkeys ophalen mislukt: HTTP {status}')
168
+ d = json.loads(body)
169
+ c = self._get_crypto(); be = c['be']()
170
+ ecdh_pub = c['lpub'](bytes.fromhex(d['ecdh_pub']), be)
171
+ kyber_pub = bytes.fromhex(d['kyber_pub']) if d.get('kyber_pub') else None
172
+ return ecdh_pub, kyber_pub
173
+
174
+ def _encrypt(self, data: bytes, ecdh_pub, kyber_pub) -> tuple:
175
+ """ML-KEM-768 + ECDH + AES-256-GCM + 5MB padding."""
176
+ c = self._get_crypto(); K = self._try_kyber(); be = c['be']()
177
+ # ECDH
178
+ eph = c['gen'](c['curve'](), be)
179
+ ecdh_ss = eph.exchange(c['ECDH'](), ecdh_pub)
180
+ eph_b = eph.public_key().public_bytes(c['Enc'].DER, c['Pub'].SubjectPublicKeyInfo)
181
+ # ML-KEM
182
+ kct = b''; kss = b''
183
+ if K and kyber_pub:
184
+ try: kct, kss = K.enc(kyber_pub)
185
+ except Exception: pass
186
+ # HKDF
187
+ ikm = ecdh_ss + kss
188
+ ss = c['HKDF'](algorithm=c['hsh'].SHA256(), length=32,
189
+ salt=b'paramant-gp-v1', info=b'aes-key', backend=be).derive(ikm)
190
+ # AES-256-GCM
191
+ nonce = os.urandom(12)
192
+ ct = c['AES'](ss).encrypt(nonce, data, None)
193
+ bundle = struct.pack('>I', len(eph_b)) + eph_b + struct.pack('>I', len(kct)) + kct
194
+ packet = struct.pack('>I', len(bundle)) + bundle + nonce + struct.pack('>I', len(ct)) + ct
195
+ if len(packet) > BLOCK:
196
+ raise GhostPipeError(f'Data te groot: {len(data)} bytes (max ~4.9MB per blok)')
197
+ blob = packet + os.urandom(BLOCK - len(packet))
198
+ return blob, hashlib.sha256(blob).hexdigest(), bool(kct)
199
+
200
+ def _decrypt(self, blob: bytes) -> bytes:
201
+ """Ontsleutel Ghost Pipe blob."""
202
+ c = self._get_crypto(); K = self._try_kyber(); be = c['be']()
203
+ kp = self._load_keypair()
204
+ o = 0
205
+ blen = struct.unpack('>I', blob[o:o+4])[0]; o += 4
206
+ bun = blob[o:o+blen]; o += blen
207
+ bo = 0
208
+ eplen = struct.unpack('>I', bun[bo:bo+4])[0]; bo += 4
209
+ epb = bun[bo:bo+eplen]; bo += eplen
210
+ klen = struct.unpack('>I', bun[bo:bo+4])[0]; bo += 4
211
+ kct = bun[bo:bo+klen]
212
+ nonce = blob[o:o+12]; o += 12
213
+ ctlen = struct.unpack('>I', blob[o:o+4])[0]; o += 4
214
+ ct = blob[o:o+ctlen]
215
+ priv = c['lpriv'](bytes.fromhex(kp['ecdh_priv']), None, be)
216
+ ecdh_ss = priv.exchange(c['ECDH'](), c['lpub'](epb, be))
217
+ kss = b''
218
+ if K and kct and kp.get('kyber_priv'):
219
+ try: kss = K.dec(bytes.fromhex(kp['kyber_priv']), kct)
220
+ except Exception: pass
221
+ ikm = ecdh_ss + kss
222
+ ss = c['HKDF'](algorithm=c['hsh'].SHA256(), length=32,
223
+ salt=b'paramant-gp-v1', info=b'aes-key', backend=be).derive(ikm)
224
+ return c['AES'](ss).decrypt(nonce, ct, None)
225
+
226
+ # ── Publieke API ──────────────────────────────────────────────────────────
227
+
228
+ def send(self, data: bytes, ttl: int = 300) -> str:
229
+ """
230
+ Verstuur data via Ghost Pipe.
231
+
232
+ Data wordt versleuteld met ML-KEM-768 + ECDH + AES-256-GCM,
233
+ gepad naar exact 5MB en geüpload naar de relay.
234
+
235
+ Args:
236
+ data: Bytes om te versturen (max ~4.9MB)
237
+ ttl: Seconden beschikbaar op relay (default 300)
238
+
239
+ Returns:
240
+ hash: SHA-256 hash — geef dit aan de ontvanger
241
+
242
+ Raises:
243
+ GhostPipeError: Bij versleuteling of upload fouten
244
+ """
245
+ ecdh_pub, kyber_pub = self._fetch_receiver_pubkeys()
246
+ blob, h, used_kyber = self._encrypt(data, ecdh_pub, kyber_pub)
247
+ body = json.dumps({
248
+ 'hash': h,
249
+ 'payload': base64.b64encode(blob).decode(),
250
+ 'ttl_ms': ttl * 1000,
251
+ 'meta': {'device_id': self.device},
252
+ }).encode()
253
+ status, resp = self._post('/v2/inbound', body)
254
+ if status != 200:
255
+ raise GhostPipeError(f'Upload mislukt: HTTP {status}: {resp.decode()[:100]}')
256
+ alg = 'ML-KEM-768+ECDH+AES-GCM' if used_kyber else 'ECDH+AES-GCM'
257
+ return h
258
+
259
+ def receive(self, hash_: str) -> bytes:
260
+ """
261
+ Ontvang data van relay via hash.
262
+ Relay vernietigt het blok direct na dit verzoek (burn-on-read).
263
+
264
+ Args:
265
+ hash_: SHA-256 hash van het blok
266
+
267
+ Returns:
268
+ Ontsleutelde data
269
+
270
+ Raises:
271
+ GhostPipeError: Blok niet gevonden, verlopen of al opgehaald
272
+ """
273
+ status, raw = self._get(f'/v2/outbound/{hash_}')
274
+ if status == 404:
275
+ raise GhostPipeError('Blok niet gevonden. Verlopen, al opgehaald, of nooit opgeslagen.')
276
+ if status != 200:
277
+ raise GhostPipeError(f'Download mislukt: HTTP {status}')
278
+ blob = base64.b64decode(raw) if len(raw) < BLOCK * 2 else raw
279
+ return self._decrypt(blob)
280
+
281
+ def status(self, hash_: str) -> dict:
282
+ """
283
+ Check of een blok beschikbaar is op de relay.
284
+
285
+ Returns:
286
+ {'available': bool, 'ttl_remaining_ms': int, 'bytes': int}
287
+ """
288
+ _, body = self._get(f'/v2/status/{hash_}')
289
+ return json.loads(body)
290
+
291
+ def receive_setup(self):
292
+ """
293
+ Initialiseer ontvanger: genereer keypair en registreer pubkeys bij relay.
294
+ Roep dit aan voordat de zender kan versturen.
295
+ """
296
+ self._load_keypair()
297
+ self._register_pubkeys()
298
+ return self
299
+
300
+ def register_webhook(self, callback_url: str, secret: str = ''):
301
+ """
302
+ Registreer webhook voor push notificaties.
303
+ Relay POSTt naar callback_url zodra een blok klaarstaat.
304
+
305
+ Args:
306
+ callback_url: URL die de relay aanroept bij nieuw blok
307
+ secret: Optioneel HMAC-SHA256 secret voor verificatie
308
+ """
309
+ body = json.dumps({'device_id': self.device, 'url': callback_url, 'secret': secret}).encode()
310
+ status, resp = self._post('/v2/webhook', body)
311
+ if status != 200:
312
+ raise GhostPipeError(f'Webhook registratie mislukt: {resp.decode()[:100]}')
313
+
314
+ def listen(self, on_receive: Callable, interval: int = 3):
315
+ """
316
+ Luister continu op nieuwe blokken (polling).
317
+
318
+ Args:
319
+ on_receive: callback(data: bytes, meta: dict) — aangeroepen bij elk blok
320
+ interval: Poll interval in seconden
321
+ """
322
+ self.receive_setup()
323
+ seq = self._load_seq()
324
+ while True:
325
+ try:
326
+ _, body = self._get('/v2/stream-next', {'device': self.device, 'seq': seq})
327
+ d = json.loads(body)
328
+ if d.get('available'):
329
+ next_seq = d.get('seq', seq + 1)
330
+ try:
331
+ data = self.receive(d['hash'])
332
+ seq = next_seq
333
+ self._save_seq(seq)
334
+ on_receive(data, {'seq': seq, 'hash': d['hash']})
335
+ continue
336
+ except GhostPipeError:
337
+ seq = next_seq
338
+ except Exception:
339
+ pass
340
+ time.sleep(interval)
341
+
342
+ def audit(self, limit: int = 100) -> list:
343
+ """Haal audit log op voor deze API key."""
344
+ _, body = self._get('/v2/audit', {'limit': limit})
345
+ return json.loads(body).get('entries', [])
346
+
347
+ def health(self) -> dict:
348
+ """Relay health check."""
349
+ _, body = self._get('/health')
350
+ return json.loads(body)
351
+
352
+ def _load_seq(self) -> int:
353
+ try:
354
+ p = os.path.join(os.path.expanduser('~/.paramant'), self.device.replace('/','_') + '.sdk_seq')
355
+ return int(open(p).read())
356
+ except Exception:
357
+ return 0
358
+
359
+ def _save_seq(self, seq: int):
360
+ d = os.path.expanduser('~/.paramant')
361
+ os.makedirs(d, exist_ok=True)
362
+ p = os.path.join(d, self.device.replace('/','_') + '.sdk_seq')
363
+ open(p + '.tmp', 'w').write(str(seq))
364
+ os.replace(p + '.tmp', p)
365
+
366
+
367
+ # ── Gebruik als script ────────────────────────────────────────────────────────
368
+ if __name__ == '__main__':
369
+ import sys, argparse
370
+
371
+ p = argparse.ArgumentParser(description=f'PARAMANT Ghost Pipe SDK v{__version__}')
372
+ p.add_argument('action', choices=['send','receive','status','listen','health','audit'])
373
+ p.add_argument('--key', required=True)
374
+ p.add_argument('--device', required=True)
375
+ p.add_argument('--relay', default='')
376
+ p.add_argument('--hash', default='')
377
+ p.add_argument('--file', default='')
378
+ p.add_argument('--ttl', type=int, default=300)
379
+ p.add_argument('--output', default='')
380
+ p.add_argument('--webhook',default='')
381
+ a = p.parse_args()
382
+
383
+ gp = GhostPipe(a.key, a.device, relay=a.relay)
384
+
385
+ if a.action == 'send':
386
+ data = open(a.file, 'rb').read() if a.file else sys.stdin.buffer.read()
387
+ gp.receive_setup()
388
+ h = gp.send(data, ttl=a.ttl)
389
+ print(f'OK hash={h}')
390
+
391
+ elif a.action == 'receive':
392
+ if not a.hash: sys.exit('--hash vereist')
393
+ gp.receive_setup()
394
+ data = gp.receive(a.hash)
395
+ if a.output:
396
+ with open(a.output, 'wb') as f: f.write(data)
397
+ print(f'OK opgeslagen in {a.output} ({len(data)} bytes)')
398
+ else:
399
+ sys.stdout.buffer.write(data)
400
+
401
+ elif a.action == 'status':
402
+ if not a.hash: sys.exit('--hash vereist')
403
+ print(json.dumps(gp.status(a.hash), indent=2))
404
+
405
+ elif a.action == 'listen':
406
+ def on_receive(data, meta):
407
+ if a.output:
408
+ path = os.path.join(a.output, f'block_{meta["seq"]:06d}.bin')
409
+ os.makedirs(a.output, exist_ok=True)
410
+ open(path, 'wb').write(data)
411
+ print(f'[RECV] seq={meta["seq"]} {len(data)}B → {path}')
412
+ else:
413
+ print(f'[RECV] seq={meta["seq"]} {len(data)}B')
414
+ if a.webhook:
415
+ gp.receive_setup()
416
+ gp.register_webhook(a.webhook)
417
+ print(f'Webhook geregistreerd: {a.webhook}')
418
+ gp.listen(on_receive)
419
+
420
+ elif a.action == 'health':
421
+ print(json.dumps(gp.health(), indent=2))
422
+
423
+ elif a.action == 'audit':
424
+ for e in gp.audit():
425
+ print(f"{e['ts']} {e['event']:<20} {e.get('hash',''):<20} {e.get('bytes',0)}B")
426
+
427
+
428
+ # ── Multi-relay failover (gossip light) ───────────────────────────────────────
429
+
430
+ class GhostPipeCluster:
431
+ """
432
+ Multi-relay client met automatische failover.
433
+ SDK vindt automatisch de dichtstbijzijnde gezonde node.
434
+
435
+ Equivalent van "gossip protocol light" — SDK pollt health
436
+ en schakelt automatisch over bij uitval.
437
+
438
+ Gebruik:
439
+ cluster = GhostPipeCluster(
440
+ api_key='pgp_xxx',
441
+ device='mri-001',
442
+ relays=[
443
+ 'https://health.paramant.app',
444
+ 'https://health-fra.paramant.app', # Frankfurt backup
445
+ 'https://health-sin.paramant.app', # Singapore backup
446
+ ]
447
+ )
448
+ hash = cluster.send(data)
449
+ """
450
+
451
+ def __init__(self, api_key: str, device: str, relays: list,
452
+ health_interval: int = 30):
453
+ self.api_key = api_key
454
+ self.device = device
455
+ self.relays = relays
456
+ self._healthy = {} # relay → last_health
457
+ self._active = None
458
+ self._lock = __import__('threading').Lock()
459
+ # Start health monitor
460
+ import threading
461
+ t = threading.Thread(target=self._monitor, daemon=True)
462
+ t.start()
463
+ # Wacht op eerste health check
464
+ time.sleep(2)
465
+
466
+ def _check_health(self, relay: str) -> dict:
467
+ try:
468
+ r = urllib.request.urlopen(
469
+ urllib.request.Request(f'{relay}/health', headers={'User-Agent': UA}),
470
+ timeout=5)
471
+ d = json.loads(r.read())
472
+ if d.get('ok'):
473
+ return {'ok': True, 'relay': relay, 'blobs': d.get('blobs', 0),
474
+ 'version': d.get('version'), 'latency_ms': 0}
475
+ except Exception:
476
+ pass
477
+ return {'ok': False, 'relay': relay}
478
+
479
+ def _monitor(self):
480
+ """Achtergrond health monitor — pollt alle relays."""
481
+ import time as t
482
+ while True:
483
+ for relay in self.relays:
484
+ health = self._check_health(relay)
485
+ with self._lock:
486
+ self._healthy[relay] = health
487
+ # Selecteer beste (eerste gezonde)
488
+ for relay in self.relays:
489
+ if self._healthy.get(relay, {}).get('ok'):
490
+ with self._lock:
491
+ if self._active != relay:
492
+ self._active = relay
493
+ break
494
+ t.sleep(30)
495
+
496
+ def _get_client(self) -> 'GhostPipe':
497
+ """Geef GhostPipe client voor actieve relay."""
498
+ with self._lock:
499
+ relay = self._active
500
+ if not relay:
501
+ raise GhostPipeError('Geen gezonde relay beschikbaar')
502
+ return GhostPipe(self.api_key, self.device, relay=relay)
503
+
504
+ def send(self, data: bytes, ttl: int = 300) -> str:
505
+ """Verstuur via eerste gezonde relay. Automatische failover."""
506
+ errors = []
507
+ for relay in self.relays:
508
+ if not self._healthy.get(relay, {}).get('ok'):
509
+ continue
510
+ try:
511
+ gp = GhostPipe(self.api_key, self.device, relay=relay)
512
+ return gp.send(data, ttl=ttl)
513
+ except GhostPipeError as e:
514
+ errors.append(f'{relay}: {e}')
515
+ # Markeer als ongezond
516
+ with self._lock:
517
+ self._healthy[relay] = {'ok': False}
518
+ raise GhostPipeError(f'Alle relays mislukt: {errors}')
519
+
520
+ def receive(self, hash_: str) -> bytes:
521
+ """Ontvang van eerste relay die het blok heeft."""
522
+ for relay in self.relays:
523
+ try:
524
+ gp = GhostPipe(self.api_key, self.device, relay=relay)
525
+ status = gp.status(hash_)
526
+ if status.get('available'):
527
+ return gp.receive(hash_)
528
+ except Exception:
529
+ pass
530
+ raise GhostPipeError('Blok niet gevonden op een van de relays')
531
+
532
+ def health(self) -> dict:
533
+ """Status van alle nodes in de cluster."""
534
+ with self._lock:
535
+ return {'active': self._active, 'nodes': dict(self._healthy)}
536
+
537
+ def receive_setup(self):
538
+ """Setup op alle gezonde relays."""
539
+ for relay in self.relays:
540
+ if self._healthy.get(relay, {}).get('ok'):
541
+ try:
542
+ GhostPipe(self.api_key, self.device, relay=relay).receive_setup()
543
+ except Exception:
544
+ pass
545
+ return self
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: paramant-sdk
3
+ Version: 1.0.0
4
+ Summary: PARAMANT Ghost Pipe SDK — post-quantum burn-on-read secure transport
5
+ Author-email: PARAMANT <hello@paramant.app>
6
+ License: BUSL-1.1
7
+ Project-URL: Homepage, https://paramant.app
8
+ Project-URL: Documentation, https://paramant.app/docs
9
+ Project-URL: Repository, https://github.com/paramant/sdk-py
10
+ Keywords: paramant,ghost-pipe,post-quantum,ml-kem,burn-on-read,secure-messaging,e2ee
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Security :: Cryptography
19
+ Classifier: Topic :: Communications
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: cryptography>=42.0
23
+ Provides-Extra: mlkem
24
+ Requires-Dist: pqcrypto; extra == "mlkem"
25
+
26
+ # paramant-sdk
27
+
28
+ PARAMANT Ghost Pipe SDK for Python. Post-quantum burn-on-read secure transport.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install paramant-sdk
34
+ ```
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ from paramant_sdk import GhostPipe
40
+
41
+ # Sender
42
+ sender = GhostPipe(api_key='pk_live_...', device='sender-001')
43
+ hash_ = sender.send(b'confidential payload')
44
+ print('hash:', hash_)
45
+
46
+ # Receiver
47
+ receiver = GhostPipe(api_key='pk_live_...', device='receiver-001')
48
+ data = receiver.receive(hash_)
49
+ print('received:', data)
50
+ # blob is burned after receive
51
+ ```
52
+
53
+ ## API
54
+
55
+ ```python
56
+ gp = GhostPipe(api_key, device, relay='', sector='health')
57
+
58
+ hash_ = gp.send(data: bytes, ttl=300) -> str
59
+ data = gp.receive(hash_: str) -> bytes
60
+ status = gp.status(hash_: str) -> dict
61
+ entries = gp.audit(limit=100) -> list
62
+ info = gp.health() -> dict
63
+ gp.listen(on_receive: callable, interval=3)
64
+ ```
65
+
66
+ ### Multi-relay cluster
67
+
68
+ ```python
69
+ from paramant_sdk import GhostPipeCluster
70
+
71
+ cluster = GhostPipeCluster(
72
+ api_key='pk_live_...',
73
+ device='device-001',
74
+ relays=['https://health.paramant.app', 'https://relay.paramant.app'],
75
+ )
76
+ hash_ = cluster.send(data)
77
+ ```
78
+
79
+ ## Protocol
80
+
81
+ - **Encryption**: ML-KEM-768 + ECDH P-256 + AES-256-GCM (hybrid PQC)
82
+ - **Burn-on-read**: blob deleted after first `receive()`
83
+ - **Sectors**: EU/DE Hetzner + Fly.io anycast
84
+
85
+ ## License
86
+
87
+ BUSL-1.1 — [paramant.app](https://paramant.app)
@@ -0,0 +1,8 @@
1
+ README.md
2
+ pyproject.toml
3
+ paramant_sdk/__init__.py
4
+ paramant_sdk.egg-info/PKG-INFO
5
+ paramant_sdk.egg-info/SOURCES.txt
6
+ paramant_sdk.egg-info/dependency_links.txt
7
+ paramant_sdk.egg-info/requires.txt
8
+ paramant_sdk.egg-info/top_level.txt
@@ -0,0 +1,4 @@
1
+ cryptography>=42.0
2
+
3
+ [mlkem]
4
+ pqcrypto
@@ -0,0 +1 @@
1
+ paramant_sdk
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "paramant-sdk"
7
+ version = "1.0.0"
8
+ description = "PARAMANT Ghost Pipe SDK — post-quantum burn-on-read secure transport"
9
+ readme = "README.md"
10
+ license = { text = "BUSL-1.1" }
11
+ authors = [{ name = "PARAMANT", email = "hello@paramant.app" }]
12
+ keywords = ["paramant", "ghost-pipe", "post-quantum", "ml-kem", "burn-on-read", "secure-messaging", "e2ee"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.9",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Security :: Cryptography",
22
+ "Topic :: Communications",
23
+ ]
24
+ requires-python = ">=3.9"
25
+ dependencies = [
26
+ "cryptography>=42.0",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ mlkem = ["pqcrypto"]
31
+
32
+ [project.urls]
33
+ Homepage = "https://paramant.app"
34
+ Documentation = "https://paramant.app/docs"
35
+ Repository = "https://github.com/paramant/sdk-py"
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["."]
39
+ include = ["paramant_sdk*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+