first commit

This commit is contained in:
duffyduck
2026-10-16 15:55:14 +02:00
commit 65e8b9440d
62 changed files with 4799 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
"""Ende-zu-Ende-Verschluesselung. Muss mit client-android/.../Crypto.kt uebereinstimmen.
Ableitung: master = PBKDF2-HMAC-SHA256(token, "netclip-v1", 200000, 32)
room_id = hex(HMAC-SHA256(master, "room-id")) -> sieht der Server
key = HMAC-SHA256(master, "enc-key") -> bleibt beim Client
Meta: base64( nonce[12] || AES-256-GCM(key, nonce, json, aad="meta") )
Blob: "NCB1" || prefix[8] || Chunks
Chunk = flag[1] || len[4, BE] || AES-256-GCM(key, prefix||counter[4, BE], data, aad=flag)
flag = 1 beim letzten Chunk (verhindert Abschneiden), sonst 0
"""
import base64
import hashlib
import hmac
import json
import os
import struct
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
SALT = b"netclip-v1"
ITERATIONS = 200_000
MAGIC = b"NCB1"
CHUNK = 1024 * 1024
class CryptoError(Exception):
pass
class Keys:
def __init__(self, token: str):
master = hashlib.pbkdf2_hmac("sha256", token.encode("utf-8"), SALT, ITERATIONS, 32)
self.room_id = hmac.new(master, b"room-id", hashlib.sha256).hexdigest()
self.key = hmac.new(master, b"enc-key", hashlib.sha256).digest()
self.aead = AESGCM(self.key)
def encrypt_meta(self, meta: dict) -> str:
nonce = os.urandom(12)
ct = self.aead.encrypt(nonce, json.dumps(meta).encode("utf-8"), b"meta")
return base64.b64encode(nonce + ct).decode("ascii")
def decrypt_meta(self, data: str) -> dict:
try:
raw = base64.b64decode(data)
return json.loads(self.aead.decrypt(raw[:12], raw[12:], b"meta"))
except Exception as e: # falscher Token oder manipuliert
raise CryptoError(str(e)) from e
def encrypt_label(self, text: str) -> str:
return self.encrypt_meta({"name": text})
def encrypt_stream(self, src, dst):
"""Liest Klartext aus src (Datei-Objekt), schreibt Blob nach dst."""
prefix = os.urandom(8)
dst.write(MAGIC + prefix)
counter = 0
buf = src.read(CHUNK)
while True:
nxt = src.read(CHUNK) if buf else b""
last = not nxt
flag = b"\x01" if last else b"\x00"
ct = self.aead.encrypt(prefix + struct.pack(">I", counter), buf, flag)
dst.write(flag + struct.pack(">I", len(ct)) + ct)
if last:
return
counter += 1
buf = nxt
def decrypt_stream(self, src, dst):
if src.read(4) != MAGIC:
raise CryptoError("kein NetClip-Blob")
prefix = src.read(8)
counter = 0
while True:
head = src.read(5)
if len(head) != 5:
raise CryptoError("Blob abgeschnitten")
flag, length = head[:1], struct.unpack(">I", head[1:])[0]
if length > CHUNK + 16:
raise CryptoError("Chunk zu gross")
ct = src.read(length)
try:
dst.write(self.aead.decrypt(prefix + struct.pack(">I", counter), ct, flag))
except Exception as e:
raise CryptoError("Entschluesselung fehlgeschlagen") from e
if flag == b"\x01":
return
counter += 1