first commit
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user