quant-bvz-secure 0.1.0__py3-none-any.whl

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.
qcrypt/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ """
2
+ qcrypt — biblioteka kryptograficzna łącząca:
3
+ 1) Post-Quantum Cryptography (PQC) — realne algorytmy odporne na komputery
4
+ kwantowe (Kyber/ML-KEM, Dilithium/ML-DSA przez liboqs, jeśli dostępne).
5
+ 2) Symulator QKD (BB84) — edukacyjna symulacja kwantowej dystrybucji klucza.
6
+ 3) Warstwę symetryczną AEAD (AES-256-GCM) do faktycznego szyfrowania danych.
7
+
8
+ WAŻNE: moduł qkd to symulacja matematyczna protokołu BB84 na klasycznym
9
+ komputerze — nie wykonuje żadnych operacji na prawdziwym sprzęcie kwantowym,
10
+ chyba że podłączysz backend Qiskit (patrz qcrypt.qkd.backend).
11
+ """
12
+
13
+ __version__ = "0.1.0"
14
+
15
+ from qcrypt.symmetric.aead import AEAD
16
+ from qcrypt.pqc.hybrid import HybridKEM
17
+ from qcrypt.qkd.bb84 import BB84Protocol
18
+ from qcrypt.exceptions import QCryptError, PQCUnavailableError, EavesdropDetected
19
+
20
+ __all__ = [
21
+ "AEAD",
22
+ "HybridKEM",
23
+ "BB84Protocol",
24
+ "QCryptError",
25
+ "PQCUnavailableError",
26
+ "EavesdropDetected",
27
+ ]
qcrypt/cli.py ADDED
@@ -0,0 +1,118 @@
1
+ """
2
+ Prosty interfejs wiersza poleceń do demonstracji biblioteki qcrypt.
3
+
4
+ Użycie:
5
+ python -m qcrypt.cli bb84-demo [--qubits N] [--eve] [--noise 0.02]
6
+ python -m qcrypt.cli hybrid-demo
7
+ python -m qcrypt.cli encrypt --key-hex <hex> --in plik.txt --out plik.enc
8
+ python -m qcrypt.cli decrypt --key-hex <hex> --in plik.enc --out plik.txt
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import sys
15
+
16
+ from qcrypt.qkd.bb84 import BB84Protocol
17
+ from qcrypt.qkd.channel import QuantumChannel
18
+ from qcrypt.pqc.hybrid import HybridKEM
19
+ from qcrypt.symmetric.aead import AEAD
20
+
21
+
22
+ def cmd_bb84_demo(args: argparse.Namespace) -> None:
23
+ channel = QuantumChannel(eavesdropper=args.eve, noise_level=args.noise)
24
+ protocol = BB84Protocol(channel=channel, n_qubits=args.qubits)
25
+ result = protocol.run()
26
+
27
+ print(f"Wysłane kubity: {result.raw_key_length}")
28
+ print(f"Po sifting: {result.sifted_key_length}")
29
+ print(f"Estymowany QBER: {result.qber:.2%}")
30
+ print(f"Eve przechwytywała: {'TAK' if args.eve else 'NIE'}")
31
+ print(f"Wykryto podsłuch: {'TAK' if result.eavesdropper_detected else 'NIE'}")
32
+ if not result.eavesdropper_detected:
33
+ print(f"Finalny klucz (hex): {result.shared_key.hex()}")
34
+ else:
35
+ print("Klucz ODRZUCONY — kanał uznany za skompromitowany.")
36
+
37
+
38
+ def cmd_hybrid_demo(args: argparse.Namespace) -> None:
39
+ bob = HybridKEM(classical_only=args.classical_only)
40
+ bundle = bob.generate_keypair()
41
+
42
+ ciphertext, alice_secret = HybridKEM.encapsulate(bundle)
43
+ bob_secret = bob.decapsulate(ciphertext)
44
+
45
+ print(f"Tryb: {'tylko klasyczny (X25519)' if bob.classical_only else 'hybrydowy (X25519 + Kyber)'}")
46
+ print(f"Sekret Alice (hex): {alice_secret.hex()}")
47
+ print(f"Sekret Boba (hex): {bob_secret.hex()}")
48
+ print(f"Sekrety zgodne: {'TAK' if alice_secret == bob_secret else 'NIE'}")
49
+
50
+ cipher = AEAD(key=alice_secret)
51
+ blob = cipher.encrypt(b"Wiadomosc zaszyfrowana wspolnym sekretem")
52
+ decrypted = AEAD(key=bob_secret).decrypt(blob)
53
+ print(f"Test AEAD: {decrypted.decode('utf-8')}")
54
+
55
+
56
+ def cmd_encrypt(args: argparse.Namespace) -> None:
57
+ key = bytes.fromhex(args.key_hex)
58
+ with open(args.infile, "rb") as f:
59
+ data = f.read()
60
+ blob = AEAD(key=key).encrypt(data)
61
+ with open(args.outfile, "wb") as f:
62
+ f.write(blob)
63
+ print(f"Zaszyfrowano -> {args.outfile}")
64
+
65
+
66
+ def cmd_decrypt(args: argparse.Namespace) -> None:
67
+ key = bytes.fromhex(args.key_hex)
68
+ with open(args.infile, "rb") as f:
69
+ blob = f.read()
70
+ data = AEAD(key=key).decrypt(blob)
71
+ with open(args.outfile, "wb") as f:
72
+ f.write(data)
73
+ print(f"Odszyfrowano -> {args.outfile}")
74
+
75
+
76
+ def build_parser() -> argparse.ArgumentParser:
77
+ parser = argparse.ArgumentParser(prog="qcrypt", description="Demo biblioteki qcrypt")
78
+ sub = parser.add_subparsers(dest="command", required=True)
79
+
80
+ p_bb84 = sub.add_parser("bb84-demo", help="Symulacja protokołu BB84")
81
+ p_bb84.add_argument("--qubits", type=int, default=1024)
82
+ p_bb84.add_argument("--eve", action="store_true", help="Włącz symulowanego podsłuchującego")
83
+ p_bb84.add_argument("--noise", type=float, default=0.0, help="Poziom szumu kanału 0.0-1.0")
84
+ p_bb84.set_defaults(func=cmd_bb84_demo)
85
+
86
+ p_hybrid = sub.add_parser("hybrid-demo", help="Hybrydowa wymiana kluczy X25519+Kyber")
87
+ p_hybrid.add_argument("--classical-only", action="store_true",
88
+ help="Wymuś tryb bez PQC (gdy liboqs niedostępne)")
89
+ p_hybrid.set_defaults(func=cmd_hybrid_demo)
90
+
91
+ p_enc = sub.add_parser("encrypt", help="Zaszyfruj plik AES-256-GCM")
92
+ p_enc.add_argument("--key-hex", required=True)
93
+ p_enc.add_argument("--in", dest="infile", required=True)
94
+ p_enc.add_argument("--out", dest="outfile", required=True)
95
+ p_enc.set_defaults(func=cmd_encrypt)
96
+
97
+ p_dec = sub.add_parser("decrypt", help="Odszyfruj plik")
98
+ p_dec.add_argument("--key-hex", required=True)
99
+ p_dec.add_argument("--in", dest="infile", required=True)
100
+ p_dec.add_argument("--out", dest="outfile", required=True)
101
+ p_dec.set_defaults(func=cmd_decrypt)
102
+
103
+ return parser
104
+
105
+
106
+ def main(argv: list[str] | None = None) -> int:
107
+ parser = build_parser()
108
+ args = parser.parse_args(argv)
109
+ try:
110
+ args.func(args)
111
+ except Exception as exc:
112
+ print(f"Blad: {exc}", file=sys.stderr)
113
+ return 1
114
+ return 0
115
+
116
+
117
+ if __name__ == "__main__":
118
+ sys.exit(main())
qcrypt/exceptions.py ADDED
@@ -0,0 +1,22 @@
1
+ """Wyjątki używane w całej bibliotece qcrypt."""
2
+
3
+
4
+ class QCryptError(Exception):
5
+ """Bazowy wyjątek biblioteki."""
6
+
7
+
8
+ class PQCUnavailableError(QCryptError):
9
+ """Podnoszony, gdy biblioteka liboqs-python (moduł `oqs`) nie jest
10
+ zainstalowana, a wywołany kod wymaga prawdziwego algorytmu PQC
11
+ (Kyber/ML-KEM, Dilithium/ML-DSA)."""
12
+
13
+
14
+ class EavesdropDetected(QCryptError):
15
+ """Podnoszony przez symulator BB84, gdy poziom błędów (QBER) w próbce
16
+ kontrolnej przekracza bezpieczny próg, co sugeruje podsłuch kanału
17
+ kwantowego."""
18
+
19
+
20
+ class DecryptionError(QCryptError):
21
+ """Podnoszony, gdy odszyfrowanie/uwierzytelnienie AEAD się nie powiedzie
22
+ (zły klucz, uszkodzone dane lub próba manipulacji)."""
qcrypt/keystore.py ADDED
@@ -0,0 +1,123 @@
1
+ """
2
+ Magazyn kluczy — serializacja kluczy publicznych/sekretów do plików JSON
3
+ (dane binarne kodowane base64). Nie przechowuje kluczy prywatnych obiektów
4
+ oqs/cryptography bezpośrednio (te żyją tylko w pamięci procesu) — przechowuje
5
+ surowe bajty.
6
+
7
+ Domyślnie (bez podania hasła) plik jest plaintext-base64 — wygodne do
8
+ szybkich testów/demo, ale NIE do produkcji. Podając `password`, cały
9
+ zawartość magazynu jest szyfrowana AES-256-GCM kluczem wyprowadzonym z hasła
10
+ przez Scrypt (odpornym na ataki brute-force na GPU/ASIC), przy użyciu
11
+ istniejącej klasy `AEAD`. Plik na dysku jest wtedy dodatkowo chroniony
12
+ uprawnieniami 0600 (tylko właściciel może czytać/pisać).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import base64
18
+ import json
19
+ import os
20
+ from pathlib import Path
21
+
22
+ from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
23
+
24
+ from qcrypt.symmetric.aead import AEAD
25
+ from qcrypt.exceptions import DecryptionError
26
+
27
+ SALT_SIZE = 16
28
+ SCRYPT_N = 2**16 # koszt CPU/pamięci — bezpieczny domyślny poziom (2026)
29
+ SCRYPT_R = 8
30
+ SCRYPT_P = 1
31
+ SCRYPT_KEY_LEN = 32
32
+
33
+
34
+ class KeyStore:
35
+ """
36
+ Przykład bez hasła (plaintext — tylko do testów/demo):
37
+ store = KeyStore("keys.json")
38
+ store.save("bob_public", public_key_bytes)
39
+ pub = store.load("bob_public")
40
+
41
+ Przykład z hasłem (zaszyfrowany magazyn — zalecane produkcyjnie):
42
+ store = KeyStore("keys.json", password="korzen-slonecznik-42")
43
+ store.save("bob_secret", secret_key_bytes)
44
+ secret = store.load("bob_secret")
45
+ """
46
+
47
+ def __init__(self, path: str | Path, *, password: str | None = None):
48
+ self.path = Path(path)
49
+ self._password = password
50
+ self._salt: bytes | None = None
51
+ self._data: dict[str, str] = {}
52
+
53
+ if self.path.exists():
54
+ self._load_from_disk()
55
+ elif password is not None:
56
+ self._salt = os.urandom(SALT_SIZE)
57
+
58
+ @property
59
+ def is_encrypted(self) -> bool:
60
+ return self._password is not None
61
+
62
+ def save(self, name: str, key_bytes: bytes) -> None:
63
+ self._data[name] = base64.b64encode(key_bytes).decode("ascii")
64
+ self._flush()
65
+
66
+ def load(self, name: str) -> bytes:
67
+ if name not in self._data:
68
+ raise KeyError(f"Brak klucza o nazwie '{name}' w {self.path}")
69
+ return base64.b64decode(self._data[name])
70
+
71
+ def delete(self, name: str) -> None:
72
+ self._data.pop(name, None)
73
+ self._flush()
74
+
75
+ def list_keys(self) -> list[str]:
76
+ return list(self._data.keys())
77
+
78
+ # -- szyfrowanie magazynu na dysku -------------------------------------
79
+
80
+ def _derive_master_key(self, salt: bytes) -> bytes:
81
+ assert self._password is not None
82
+ kdf = Scrypt(salt=salt, length=SCRYPT_KEY_LEN, n=SCRYPT_N, r=SCRYPT_R, p=SCRYPT_P)
83
+ return kdf.derive(self._password.encode("utf-8"))
84
+
85
+ def _load_from_disk(self) -> None:
86
+ raw = self.path.read_bytes()
87
+
88
+ if self._password is None:
89
+ # tryb plaintext (kompatybilność wsteczna / demo)
90
+ self._data = json.loads(raw.decode("utf-8"))
91
+ return
92
+
93
+ if len(raw) < SALT_SIZE:
94
+ raise DecryptionError(f"Plik {self.path} jest za krótki, brak salt.")
95
+ salt, blob = raw[:SALT_SIZE], raw[SALT_SIZE:]
96
+ self._salt = salt
97
+ master_key = self._derive_master_key(salt)
98
+ try:
99
+ plaintext = AEAD(key=master_key).decrypt(blob)
100
+ except DecryptionError as exc:
101
+ raise DecryptionError(
102
+ f"Nie udało się odszyfrować magazynu {self.path} — błędne hasło "
103
+ "lub plik został uszkodzony/zmanipulowany."
104
+ ) from exc
105
+ self._data = json.loads(plaintext.decode("utf-8"))
106
+
107
+ def _flush(self) -> None:
108
+ tmp_path = self.path.with_suffix(".tmp")
109
+ payload = json.dumps(self._data, indent=2).encode("utf-8")
110
+
111
+ if self._password is not None:
112
+ if self._salt is None:
113
+ self._salt = os.urandom(SALT_SIZE)
114
+ master_key = self._derive_master_key(self._salt)
115
+ payload = self._salt + AEAD(key=master_key).encrypt(payload)
116
+
117
+ # Uprawnienia 0600 od razu przy tworzeniu pliku (nie po fakcie —
118
+ # unikamy okna czasowego, w którym plik byłby czytelny dla innych).
119
+ fd = os.open(tmp_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
120
+ with os.fdopen(fd, "wb") as f:
121
+ f.write(payload)
122
+ os.replace(tmp_path, self.path)
123
+ os.chmod(self.path, 0o600)
qcrypt/pqc/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from qcrypt.pqc.hybrid import HybridKEM
2
+ from qcrypt.pqc.kem import KyberKEM, PQC_AVAILABLE
3
+ from qcrypt.pqc.signatures import DilithiumSigner
4
+
5
+ __all__ = ["HybridKEM", "KyberKEM", "DilithiumSigner", "PQC_AVAILABLE"]
qcrypt/pqc/hybrid.py ADDED
@@ -0,0 +1,155 @@
1
+ """
2
+ Hybrydowa wymiana klucza: X25519 (klasyczny, eliptyczny) + Kyber/ML-KEM
3
+ (post-kwantowy), połączone przez HKDF w jeden wspólny sekret.
4
+
5
+ To zalecany przez NIST tryb na okres przejściowy: jeśli Kyber okaże się mieć
6
+ niewykrytą słabość, bezpieczeństwo nadal opiera się na X25519; jeśli komputery
7
+ kwantowe złamią X25519, bezpieczeństwo nadal opiera się na Kyber. Atakujący
8
+ musi złamać OBA, żeby odzyskać sekret.
9
+
10
+ Jeśli liboqs-python nie jest zainstalowane, klasa może działać w trybie
11
+ `classical_only=True` — tylko X25519, bez odporności kwantowej (jawnie
12
+ oznaczone ostrzeżeniem).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import warnings
18
+ from dataclasses import dataclass
19
+
20
+ from cryptography.hazmat.primitives.asymmetric.x25519 import (
21
+ X25519PrivateKey,
22
+ X25519PublicKey,
23
+ )
24
+ from cryptography.hazmat.primitives import hashes
25
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
26
+ from cryptography.hazmat.primitives import serialization
27
+
28
+ from qcrypt.pqc.kem import KyberKEM, PQC_AVAILABLE, DEFAULT_ALGORITHM as KYBER_ALG
29
+
30
+
31
+ @dataclass
32
+ class HybridPublicBundle:
33
+ """To, co strona odpowiadająca wysyła do strony inicjującej połączenie."""
34
+
35
+ x25519_public: bytes
36
+ kyber_public: bytes | None # None w trybie classical_only
37
+
38
+ def to_bytes(self) -> bytes:
39
+ kp = self.kyber_public or b""
40
+ return len(self.x25519_public).to_bytes(2, "big") + self.x25519_public + kp
41
+
42
+ @classmethod
43
+ def from_bytes(cls, blob: bytes) -> "HybridPublicBundle":
44
+ x_len = int.from_bytes(blob[:2], "big")
45
+ x25519_public = blob[2:2 + x_len]
46
+ kyber_public = blob[2 + x_len:] or None
47
+ return cls(x25519_public=x25519_public, kyber_public=kyber_public)
48
+
49
+
50
+ @dataclass
51
+ class HybridCiphertext:
52
+ """To, co strona inicjująca wysyła z powrotem po encapsulate()."""
53
+
54
+ x25519_ephemeral_public: bytes
55
+ kyber_ciphertext: bytes | None
56
+
57
+ def to_bytes(self) -> bytes:
58
+ kc = self.kyber_ciphertext or b""
59
+ return len(self.x25519_ephemeral_public).to_bytes(2, "big") + self.x25519_ephemeral_public + kc
60
+
61
+ @classmethod
62
+ def from_bytes(cls, blob: bytes) -> "HybridCiphertext":
63
+ x_len = int.from_bytes(blob[:2], "big")
64
+ x25519_ephemeral_public = blob[2:2 + x_len]
65
+ kyber_ciphertext = blob[2 + x_len:] or None
66
+ return cls(x25519_ephemeral_public=x25519_ephemeral_public, kyber_ciphertext=kyber_ciphertext)
67
+
68
+
69
+ class HybridKEM:
70
+ """
71
+ Hybrydowa wymiana klucza X25519 + Kyber.
72
+
73
+ Strona odpowiadająca (Bob):
74
+ bob = HybridKEM()
75
+ bundle = bob.generate_keypair() # -> wysyłamy do Alice
76
+
77
+ Strona inicjująca (Alice):
78
+ ciphertext, shared_secret = HybridKEM.encapsulate(bundle) # -> ciphertext do Boba
79
+
80
+ Strona odpowiadająca:
81
+ shared_secret = bob.decapsulate(ciphertext) # ten sam sekret co u Alice
82
+ """
83
+
84
+ def __init__(self, *, classical_only: bool = False):
85
+ self.classical_only = classical_only or not PQC_AVAILABLE
86
+ if self.classical_only and PQC_AVAILABLE is False and not classical_only:
87
+ warnings.warn(
88
+ "liboqs-python niedostępne — HybridKEM działa w trybie WYŁĄCZNIE "
89
+ "KLASYCZNYM (X25519), bez odporności na komputery kwantowe. "
90
+ "Zainstaluj 'liboqs-python', aby włączyć warstwę PQC.",
91
+ RuntimeWarning,
92
+ stacklevel=2,
93
+ )
94
+ self._x25519_private = X25519PrivateKey.generate()
95
+ self._kyber = None if self.classical_only else KyberKEM()
96
+
97
+ def generate_keypair(self) -> HybridPublicBundle:
98
+ x_pub = self._x25519_private.public_key().public_bytes(
99
+ encoding=serialization.Encoding.Raw,
100
+ format=serialization.PublicFormat.Raw,
101
+ )
102
+ k_pub = None if self.classical_only else self._kyber.generate_keypair()
103
+ return HybridPublicBundle(x25519_public=x_pub, kyber_public=k_pub)
104
+
105
+ @staticmethod
106
+ def encapsulate(bundle: HybridPublicBundle) -> tuple[HybridCiphertext, bytes]:
107
+ """Strona inicjująca: zwraca (ciphertext_do_wyslania, wspolny_sekret)."""
108
+ # --- warstwa klasyczna: efemeryczny X25519 ---
109
+ ephemeral_private = X25519PrivateKey.generate()
110
+ ephemeral_public_bytes = ephemeral_private.public_key().public_bytes(
111
+ encoding=serialization.Encoding.Raw,
112
+ format=serialization.PublicFormat.Raw,
113
+ )
114
+ peer_public = X25519PublicKey.from_public_bytes(bundle.x25519_public)
115
+ x25519_secret = ephemeral_private.exchange(peer_public)
116
+
117
+ # --- warstwa post-kwantowa: Kyber (jeśli dostępna) ---
118
+ kyber_ciphertext = None
119
+ kyber_secret = b""
120
+ if bundle.kyber_public is not None:
121
+ kyber_ciphertext, kyber_secret = KyberKEM.encapsulate(
122
+ bundle.kyber_public, algorithm=KYBER_ALG
123
+ )
124
+
125
+ combined_secret = HybridKEM._combine(x25519_secret, kyber_secret)
126
+ ciphertext = HybridCiphertext(
127
+ x25519_ephemeral_public=ephemeral_public_bytes,
128
+ kyber_ciphertext=kyber_ciphertext,
129
+ )
130
+ return ciphertext, combined_secret
131
+
132
+ def decapsulate(self, ciphertext: HybridCiphertext) -> bytes:
133
+ """Strona odpowiadająca: odzyskuje ten sam wspólny sekret."""
134
+ peer_ephemeral = X25519PublicKey.from_public_bytes(ciphertext.x25519_ephemeral_public)
135
+ x25519_secret = self._x25519_private.exchange(peer_ephemeral)
136
+
137
+ kyber_secret = b""
138
+ if ciphertext.kyber_ciphertext is not None:
139
+ if self._kyber is None:
140
+ raise ValueError("Otrzymano ciphertext Kyber, ale ten KEM działa w trybie classical_only.")
141
+ kyber_secret = self._kyber.decapsulate(ciphertext.kyber_ciphertext)
142
+
143
+ return self._combine(x25519_secret, kyber_secret)
144
+
145
+ @staticmethod
146
+ def _combine(x25519_secret: bytes, kyber_secret: bytes) -> bytes:
147
+ """Łączy oba sekrety przez HKDF — standardowa konstrukcja hybrydowa
148
+ (zgodna z duchem zaleceń NIST/IETF dot. hybrydowego KEM)."""
149
+ hkdf = HKDF(
150
+ algorithm=hashes.SHA256(),
151
+ length=32,
152
+ salt=None,
153
+ info=b"qcrypt-hybrid-kem-v1",
154
+ )
155
+ return hkdf.derive(x25519_secret + kyber_secret)
qcrypt/pqc/kem.py ADDED
@@ -0,0 +1,96 @@
1
+ """
2
+ Mechanizm uzgadniania klucza (KEM) odporny na komputery kwantowe.
3
+
4
+ Owija bibliotekę `liboqs-python` (pakiet `oqs`), która dostarcza zaudytowane
5
+ implementacje algorytmów kratowych, w tym Kyber / ML-KEM (standard NIST
6
+ FIPS 203). Nie implementujemy tu własnej kryptografii kratowej — to zadanie
7
+ zostawiamy zweryfikowanej bibliotece.
8
+
9
+ Instalacja:
10
+ pip install liboqs-python
11
+ (wymaga skompilowania biblioteki C `liboqs` — patrz README).
12
+
13
+ Jeśli `oqs` nie jest zainstalowane, klasa `KyberKEM` jest zdefiniowana, ale
14
+ każda próba jej użycia rzuca PQCUnavailableError z instrukcją instalacji.
15
+ Dzięki temu reszta biblioteki (np. HybridKEM) może działać w trybie
16
+ "tylko klasyczny" bez twardej zależności.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass
22
+
23
+ from qcrypt.exceptions import PQCUnavailableError
24
+
25
+ try:
26
+ import oqs # type: ignore
27
+
28
+ PQC_AVAILABLE = True
29
+ except ImportError:
30
+ oqs = None # type: ignore
31
+ PQC_AVAILABLE = False
32
+
33
+ DEFAULT_ALGORITHM = "Kyber768" # odpowiednik ML-KEM-768 (NIST poziom 3)
34
+
35
+
36
+ @dataclass
37
+ class KEMKeypair:
38
+ algorithm: str
39
+ public_key: bytes
40
+ # sekretny stan trzymany po stronie serwera oqs.KeyEncapsulation,
41
+ # niedostępny bezpośrednio — patrz KyberKEM._client
42
+
43
+
44
+ class KyberKEM:
45
+ """
46
+ Wymiana klucza metodą Kyber/ML-KEM.
47
+
48
+ Przykład:
49
+ bob = KyberKEM()
50
+ public_key = bob.generate_keypair()
51
+
52
+ # Alice:
53
+ ciphertext, alice_secret = KyberKEM.encapsulate(public_key)
54
+
55
+ # Bob:
56
+ bob_secret = bob.decapsulate(ciphertext)
57
+ assert alice_secret == bob_secret
58
+ """
59
+
60
+ def __init__(self, algorithm: str = DEFAULT_ALGORITHM):
61
+ self._require_pqc()
62
+ self.algorithm = algorithm
63
+ self._kem = oqs.KeyEncapsulation(algorithm)
64
+ self._public_key: bytes | None = None
65
+
66
+ @staticmethod
67
+ def _require_pqc() -> None:
68
+ if not PQC_AVAILABLE:
69
+ raise PQCUnavailableError(
70
+ "Pakiet 'oqs' (liboqs-python) nie jest zainstalowany. "
71
+ "Zainstaluj: pip install liboqs-python "
72
+ "(wymaga skompilowanej biblioteki C liboqs). "
73
+ "Alternatywnie użyj HybridKEM w trybie 'classical_only=True', "
74
+ "który działa bez PQC (tylko X25519, bez odporności kwantowej)."
75
+ )
76
+
77
+ def generate_keypair(self) -> bytes:
78
+ """Generuje parę kluczy, zwraca klucz publiczny do przesłania drugiej stronie."""
79
+ self._public_key = self._kem.generate_keypair()
80
+ return self._public_key
81
+
82
+ @staticmethod
83
+ def encapsulate(public_key: bytes, algorithm: str = DEFAULT_ALGORITHM) -> tuple[bytes, bytes]:
84
+ """Strona inicjująca: na podstawie klucza publicznego drugiej strony
85
+ generuje (ciphertext, shared_secret). ciphertext wysyłamy do właściciela
86
+ klucza publicznego, shared_secret zostaje lokalnie."""
87
+ KyberKEM._require_pqc()
88
+ with oqs.KeyEncapsulation(algorithm) as kem:
89
+ ciphertext, shared_secret = kem.encap_secret(public_key)
90
+ return ciphertext, shared_secret
91
+
92
+ def decapsulate(self, ciphertext: bytes) -> bytes:
93
+ """Strona posiadająca klucz prywatny: odzyskuje ten sam shared_secret."""
94
+ if self._public_key is None:
95
+ raise RuntimeError("Najpierw wywołaj generate_keypair().")
96
+ return self._kem.decap_secret(ciphertext)
@@ -0,0 +1,58 @@
1
+ """
2
+ Podpisy cyfrowe odporne na komputery kwantowe — Dilithium / ML-DSA
3
+ (standard NIST FIPS 204), przez liboqs-python.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from qcrypt.exceptions import PQCUnavailableError
9
+ from qcrypt.pqc.kem import PQC_AVAILABLE
10
+
11
+ try:
12
+ import oqs # type: ignore
13
+ except ImportError:
14
+ oqs = None # type: ignore
15
+
16
+ DEFAULT_ALGORITHM = "Dilithium3" # odpowiednik ML-DSA-65 (NIST poziom 3)
17
+
18
+
19
+ class DilithiumSigner:
20
+ """
21
+ Podpisywanie i weryfikacja odporne na kwantowe.
22
+
23
+ Przykład:
24
+ signer = DilithiumSigner()
25
+ public_key = signer.generate_keypair()
26
+ signature = signer.sign(b"wiadomość")
27
+
28
+ assert DilithiumSigner.verify(public_key, b"wiadomość", signature)
29
+ """
30
+
31
+ def __init__(self, algorithm: str = DEFAULT_ALGORITHM):
32
+ if not PQC_AVAILABLE:
33
+ raise PQCUnavailableError(
34
+ "Pakiet 'oqs' (liboqs-python) nie jest zainstalowany. "
35
+ "Zainstaluj: pip install liboqs-python"
36
+ )
37
+ self.algorithm = algorithm
38
+ self._signer = oqs.Signature(algorithm)
39
+ self._public_key: bytes | None = None
40
+
41
+ def generate_keypair(self) -> bytes:
42
+ self._public_key = self._signer.generate_keypair()
43
+ return self._public_key
44
+
45
+ def sign(self, message: bytes) -> bytes:
46
+ if self._public_key is None:
47
+ raise RuntimeError("Najpierw wywołaj generate_keypair().")
48
+ return self._signer.sign(message)
49
+
50
+ @staticmethod
51
+ def verify(public_key: bytes, message: bytes, signature: bytes,
52
+ algorithm: str = DEFAULT_ALGORITHM) -> bool:
53
+ if not PQC_AVAILABLE:
54
+ raise PQCUnavailableError(
55
+ "Pakiet 'oqs' (liboqs-python) nie jest zainstalowany."
56
+ )
57
+ with oqs.Signature(algorithm) as verifier:
58
+ return verifier.verify(message, signature, public_key)
qcrypt/qkd/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from qcrypt.qkd.bb84 import BB84Protocol, BB84Result
2
+ from qcrypt.qkd.channel import QuantumChannel
3
+
4
+ __all__ = ["BB84Protocol", "BB84Result", "QuantumChannel"]
qcrypt/qkd/backend.py ADDED
@@ -0,0 +1,88 @@
1
+ """
2
+ Abstrakcja "backendu kubitowego" dla protokołu BB84.
3
+
4
+ Domyślny `SimulatedBackend` to czysta matematyka (żadnych obwodów kwantowych) —
5
+ bit + baza kodowania są reprezentowane jako para liczb, a pomiar w złej bazie
6
+ daje losowy wynik zgodnie z regułami mechaniki kwantowej dla stanów BB84
7
+ (co matematycznie odtwarza zachowanie prawdziwych qubitów dla tego protokołu).
8
+
9
+ `QiskitBackend` (opcjonalny) faktycznie buduje obwód kwantowy dla każdego
10
+ kubitu i wykonuje go na symulatorze Qiskit Aer (lub, po zmianie `service`,
11
+ na prawdziwym komputerze kwantowym IBM). Wymaga `pip install qiskit qiskit-aer`.
12
+
13
+ Bazy: 0 = baza obliczeniowa (Z, "+"), 1 = baza diagonalna (X, "x")
14
+ Bity: 0 lub 1
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import random
20
+ from abc import ABC, abstractmethod
21
+
22
+
23
+ class QubitBackend(ABC):
24
+ """Interfejs backendu: przygotuj kubit w danym bicie/bazie, zmierz w danej bazie."""
25
+
26
+ @abstractmethod
27
+ def measure(self, sent_bit: int, sent_basis: int, measure_basis: int) -> int:
28
+ """Zwraca wynik pomiaru kubitu wysłanego jako (sent_bit, sent_basis),
29
+ zmierzonego w bazie measure_basis. Jeśli bazy się zgadzają, wynik = sent_bit
30
+ (w idealnym kanale). Jeśli się nie zgadzają, wynik jest losowy (50/50)."""
31
+ raise NotImplementedError
32
+
33
+
34
+ class SimulatedBackend(QubitBackend):
35
+ """Domyślny backend: symulacja matematyczna bez faktycznych obwodów
36
+ kwantowych. Szybki, deterministyczny (poza losowością pomiaru w złej
37
+ bazie), używa `random.SystemRandom` jako źródła entropii."""
38
+
39
+ def __init__(self, seed: int | None = None):
40
+ self._rng = random.SystemRandom() if seed is None else random.Random(seed)
41
+
42
+ def measure(self, sent_bit: int, sent_basis: int, measure_basis: int) -> int:
43
+ if sent_basis == measure_basis:
44
+ return sent_bit
45
+ return self._rng.randint(0, 1)
46
+
47
+
48
+ class QiskitBackend(QubitBackend):
49
+ """
50
+ Opcjonalny backend wykonujący faktyczny obwód kwantowy dla każdego
51
+ kubitu na symulatorze Qiskit Aer.
52
+
53
+ Wymaga: pip install qiskit qiskit-aer
54
+
55
+ Kodowanie:
56
+ bit=0, basis=0 (Z) -> |0>
57
+ bit=1, basis=0 (Z) -> |1>
58
+ bit=0, basis=1 (X) -> |+> (H|0>)
59
+ bit=1, basis=1 (X) -> |-> (H|1>)
60
+ Pomiar w bazie X realizowany jako H przed pomiarem w bazie obliczeniowej.
61
+ """
62
+
63
+ def __init__(self):
64
+ try:
65
+ from qiskit import QuantumCircuit
66
+ from qiskit_aer import AerSimulator
67
+ except ImportError as exc:
68
+ raise ImportError(
69
+ "QiskitBackend wymaga pakietów 'qiskit' i 'qiskit-aer'. "
70
+ "Zainstaluj: pip install qiskit qiskit-aer"
71
+ ) from exc
72
+ self._QuantumCircuit = QuantumCircuit
73
+ self._simulator = AerSimulator()
74
+
75
+ def measure(self, sent_bit: int, sent_basis: int, measure_basis: int) -> int:
76
+ qc = self._QuantumCircuit(1, 1)
77
+ if sent_bit == 1:
78
+ qc.x(0)
79
+ if sent_basis == 1:
80
+ qc.h(0)
81
+ if measure_basis == 1:
82
+ qc.h(0)
83
+ qc.measure(0, 0)
84
+
85
+ job = self._simulator.run(qc, shots=1, memory=True)
86
+ result = job.result()
87
+ (outcome,) = result.get_memory(qc)
88
+ return int(outcome)
qcrypt/qkd/bb84.py ADDED
@@ -0,0 +1,160 @@
1
+ """
2
+ Protokół BB84 (Bennett–Brassard 1984) — symulacja kwantowej dystrybucji klucza.
3
+
4
+ Etapy:
5
+ 1. Alice generuje losowe bity i losowe bazy, "wysyła" kubity.
6
+ 2. Bob mierzy każdy kubit w losowo wybranej bazie.
7
+ 3. Sifting: Alice i Bob porównują (jawnie, klasycznym kanałem) użyte bazy —
8
+ zachowują tylko bity, gdzie bazy się zgodziły.
9
+ 4. Estymacja QBER: poświęcają losową próbkę przesianych bitów, porównują
10
+ jawnie — jeśli błędów jest za dużo, ktoś podsłuchiwał -> przerywają.
11
+ 5. Privacy amplification: pozostały klucz jest haszowany (SHA-256), żeby
12
+ usunąć ewentualną szczątkową wiedzę podsłuchującego o kluczu.
13
+
14
+ UWAGA: to symulacja matematyczna na klasycznym komputerze. Domyślny
15
+ `SimulatedBackend` nie wykonuje żadnych operacji na prawdziwym sprzęcie
16
+ kwantowym — służy do nauki protokołu i integracji z resztą biblioteki.
17
+ Do faktycznych obwodów kwantowych podłącz `qcrypt.qkd.backend.QiskitBackend`.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import hashlib
23
+ import random
24
+ from dataclasses import dataclass, field
25
+
26
+ from qcrypt.exceptions import EavesdropDetected
27
+ from qcrypt.qkd.channel import QuantumChannel
28
+
29
+ DEFAULT_QBER_THRESHOLD = 0.11 # ~11%, próg klasycznie przyjmowany dla BB84
30
+ DEFAULT_SAMPLE_FRACTION = 0.25 # jaka część przesianych bitów idzie na test QBER
31
+
32
+
33
+ @dataclass
34
+ class BB84Result:
35
+ """Wynik przebiegu protokołu."""
36
+
37
+ raw_key_length: int # liczba wysłanych kubitów
38
+ sifted_key_length: int # po odrzuceniu niezgodnych baz
39
+ final_key_length: int # po odjęciu próbki testowej
40
+ qber: float # estymowany współczynnik błędów
41
+ eavesdropper_detected: bool
42
+ shared_key: bytes # finalny klucz (po privacy amplification), pusty jeśli przerwano
43
+
44
+
45
+ class BB84Protocol:
46
+ """
47
+ Orkiestracja pełnego przebiegu BB84 między Alice a Bobem.
48
+
49
+ Przykład:
50
+ channel = QuantumChannel(eavesdropper=False, noise_level=0.02)
51
+ protocol = BB84Protocol(channel=channel, n_qubits=2048)
52
+ result = protocol.run()
53
+
54
+ if result.eavesdropper_detected:
55
+ print("Kanał skompromitowany — klucz odrzucony.")
56
+ else:
57
+ secret_key_bytes = result.shared_key # gotowe do użycia w AEAD
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ channel: QuantumChannel | None = None,
63
+ *,
64
+ n_qubits: int = 1024,
65
+ qber_threshold: float = DEFAULT_QBER_THRESHOLD,
66
+ sample_fraction: float = DEFAULT_SAMPLE_FRACTION,
67
+ seed: int | None = None,
68
+ ):
69
+ self.channel = channel or QuantumChannel(seed=seed)
70
+ self.n_qubits = n_qubits
71
+ self.qber_threshold = qber_threshold
72
+ self.sample_fraction = sample_fraction
73
+ self._rng = random.SystemRandom() if seed is None else random.Random(seed)
74
+
75
+ def run(self) -> BB84Result:
76
+ # --- 1. Alice generuje losowe bity i bazy ---
77
+ alice_bits = [self._rng.randint(0, 1) for _ in range(self.n_qubits)]
78
+ alice_bases = [self._rng.randint(0, 1) for _ in range(self.n_qubits)]
79
+
80
+ # --- 2. Transmisja + pomiar Boba ---
81
+ bob_bases = [self._rng.randint(0, 1) for _ in range(self.n_qubits)]
82
+ bob_bits: list[int] = []
83
+ for bit, basis, bob_basis in zip(alice_bits, alice_bases, bob_bases):
84
+ tx = self.channel.transmit(bit, basis)
85
+ bob_bits.append(self.channel.measure(tx, bob_basis))
86
+
87
+ # --- 3. Sifting: zostają tylko pozycje ze zgodną bazą ---
88
+ sifted_alice, sifted_bob = [], []
89
+ for a_bit, a_basis, b_basis, b_bit in zip(alice_bits, alice_bases, bob_bases, bob_bits):
90
+ if a_basis == b_basis:
91
+ sifted_alice.append(a_bit)
92
+ sifted_bob.append(b_bit)
93
+
94
+ sifted_len = len(sifted_alice)
95
+ if sifted_len == 0:
96
+ return BB84Result(
97
+ raw_key_length=self.n_qubits,
98
+ sifted_key_length=0,
99
+ final_key_length=0,
100
+ qber=1.0,
101
+ eavesdropper_detected=True,
102
+ shared_key=b"",
103
+ )
104
+
105
+ # --- 4. Estymacja QBER na losowej próbce ---
106
+ sample_size = max(1, int(sifted_len * self.sample_fraction))
107
+ sample_indices = set(self._rng.sample(range(sifted_len), sample_size))
108
+
109
+ mismatches = sum(
110
+ 1 for i in sample_indices if sifted_alice[i] != sifted_bob[i]
111
+ )
112
+ qber = mismatches / sample_size
113
+
114
+ eavesdropper_detected = qber > self.qber_threshold
115
+
116
+ # --- 5. Usunięcie próbki testowej z klucza (ujawniona jawnie) ---
117
+ remaining_alice = [
118
+ b for i, b in enumerate(sifted_alice) if i not in sample_indices
119
+ ]
120
+
121
+ if eavesdropper_detected:
122
+ return BB84Result(
123
+ raw_key_length=self.n_qubits,
124
+ sifted_key_length=sifted_len,
125
+ final_key_length=0,
126
+ qber=qber,
127
+ eavesdropper_detected=True,
128
+ shared_key=b"",
129
+ )
130
+
131
+ # --- 6. Privacy amplification: hash klucza do finalnego sekretu ---
132
+ final_key = self._privacy_amplify(remaining_alice)
133
+
134
+ return BB84Result(
135
+ raw_key_length=self.n_qubits,
136
+ sifted_key_length=sifted_len,
137
+ final_key_length=len(remaining_alice),
138
+ qber=qber,
139
+ eavesdropper_detected=False,
140
+ shared_key=final_key,
141
+ )
142
+
143
+ def run_or_raise(self) -> bytes:
144
+ """Wygodny wariant: zwraca klucz albo rzuca EavesdropDetected."""
145
+ result = self.run()
146
+ if result.eavesdropper_detected:
147
+ raise EavesdropDetected(
148
+ f"Wykryto podsłuch kanału kwantowego: QBER={result.qber:.2%} "
149
+ f"przekracza próg {self.qber_threshold:.2%}. Klucz odrzucony."
150
+ )
151
+ return result.shared_key
152
+
153
+ @staticmethod
154
+ def _privacy_amplify(bits: list[int]) -> bytes:
155
+ """Redukuje przesiany klucz do finalnego sekretu poprzez hashowanie —
156
+ usuwa szczątkową informację, jaką mógłby zdobyć podsłuchujący,
157
+ nawet jeśli QBER był poniżej progu wykrywalności."""
158
+ bit_string = "".join(str(b) for b in bits)
159
+ packed = int(bit_string, 2).to_bytes((len(bit_string) + 7) // 8, "big") if bit_string else b""
160
+ return hashlib.sha256(packed).digest()
qcrypt/qkd/channel.py ADDED
@@ -0,0 +1,80 @@
1
+ """
2
+ Model kanału kwantowego łączącego Alice i Boba w symulacji BB84.
3
+
4
+ Obsługuje dwa źródła błędów, które w realnym QKD są nie do odróżnienia
5
+ (i właśnie dlatego wysoki QBER traktuje się jako "być może podsłuch"):
6
+ - `noise_level`: naturalny szum/straty w kanale (imperfekcje sprzętu),
7
+ - `eavesdropper`: symulacja ataku "przechwyć i wyślij ponownie" (intercept-resend).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import random
13
+ from dataclasses import dataclass, field
14
+
15
+ from qcrypt.qkd.backend import QubitBackend, SimulatedBackend
16
+
17
+
18
+ @dataclass
19
+ class TransmittedQubit:
20
+ """Stan pojedynczego kubitu w locie: to, co faktycznie wysyła Alice
21
+ (po ewentualnej ingerencji Eve)."""
22
+
23
+ original_bit: int
24
+ original_basis: int
25
+ sent_bit: int # może się różnić od original_bit, jeśli Eve zmierzyła w złej bazie
26
+ sent_basis: int # bazuje na tym, co Eve retransmituje (=original_basis jeśli brak Eve)
27
+ intercepted: bool
28
+
29
+
30
+ class QuantumChannel:
31
+ """
32
+ Symulowany kanał kwantowy między Alice a Bobem.
33
+
34
+ Args:
35
+ backend: implementacja pomiaru kubitów (domyślnie SimulatedBackend).
36
+ noise_level: prawdopodobieństwo losowego przekłamania bitu w kanale (0.0–1.0).
37
+ eavesdropper: jeśli True, Eve przechwytuje każdy kubit metodą
38
+ intercept-resend (mierzy w losowej bazie, wysyła dalej to, co zmierzyła).
39
+ seed: ziarno RNG dla powtarzalności testów.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ backend: QubitBackend | None = None,
45
+ *,
46
+ noise_level: float = 0.0,
47
+ eavesdropper: bool = False,
48
+ seed: int | None = None,
49
+ ):
50
+ self.backend = backend or SimulatedBackend(seed=seed)
51
+ self.noise_level = noise_level
52
+ self.eavesdropper = eavesdropper
53
+ self._rng = random.SystemRandom() if seed is None else random.Random(seed)
54
+
55
+ def transmit(self, bit: int, basis: int) -> TransmittedQubit:
56
+ """Wysyła jeden kubit (bit zakodowany w danej bazie) od Alice do Boba,
57
+ symulując ewentualny podsłuch Eve i szum kanału."""
58
+ sent_bit, sent_basis, intercepted = bit, basis, False
59
+
60
+ if self.eavesdropper:
61
+ eve_basis = self._rng.randint(0, 1)
62
+ eve_measured = self.backend.measure(bit, basis, eve_basis)
63
+ # Eve retransmituje kubit zakodowany zgodnie z tym, co zmierzyła —
64
+ # to właśnie wprowadza wykrywalne błędy, gdy jej baza się nie zgadza z Alice.
65
+ sent_bit, sent_basis, intercepted = eve_measured, eve_basis, True
66
+
67
+ if self._rng.random() < self.noise_level:
68
+ sent_bit ^= 1 # losowe przekłamanie bitu przez szum kanału
69
+
70
+ return TransmittedQubit(
71
+ original_bit=bit,
72
+ original_basis=basis,
73
+ sent_bit=sent_bit,
74
+ sent_basis=sent_basis,
75
+ intercepted=intercepted,
76
+ )
77
+
78
+ def measure(self, tx: TransmittedQubit, measure_basis: int) -> int:
79
+ """Bob mierzy otrzymany kubit w wybranej przez siebie bazie."""
80
+ return self.backend.measure(tx.sent_bit, tx.sent_basis, measure_basis)
@@ -0,0 +1,3 @@
1
+ from qcrypt.symmetric.aead import AEAD
2
+
3
+ __all__ = ["AEAD"]
@@ -0,0 +1,90 @@
1
+ """
2
+ Warstwa symetryczna AEAD (Authenticated Encryption with Associated Data).
3
+
4
+ Klucz otrzymany z wymiany kluczy (PQC KEM) lub z symulacji BB84 ma zwykle
5
+ nieregularną długość / jest surowym sekretem — tutaj przepuszczamy go przez
6
+ HKDF, aby uzyskać właściwy 256-bitowy klucz AES, a następnie szyfrujemy dane
7
+ w trybie AES-256-GCM (poufność + integralność w jednym).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ from dataclasses import dataclass
14
+
15
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
16
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
17
+ from cryptography.hazmat.primitives import hashes
18
+
19
+ from qcrypt.exceptions import DecryptionError
20
+
21
+ NONCE_SIZE = 12 # 96 bitów — zalecane dla GCM
22
+ KEY_SIZE = 32 # 256 bitów
23
+
24
+
25
+ @dataclass
26
+ class EncryptedMessage:
27
+ """Kontener na wynik szyfrowania: nonce + szyfrogram (z tagiem GCM)."""
28
+
29
+ nonce: bytes
30
+ ciphertext: bytes
31
+
32
+ def to_bytes(self) -> bytes:
33
+ """Serializuje do jednego blobu: [nonce (12B)][ciphertext+tag]."""
34
+ return self.nonce + self.ciphertext
35
+
36
+ @classmethod
37
+ def from_bytes(cls, blob: bytes) -> "EncryptedMessage":
38
+ if len(blob) < NONCE_SIZE:
39
+ raise DecryptionError("Blob zbyt krótki, brak pełnego nonce.")
40
+ return cls(nonce=blob[:NONCE_SIZE], ciphertext=blob[NONCE_SIZE:])
41
+
42
+
43
+ class AEAD:
44
+ """
45
+ Szyfrowanie symetryczne AES-256-GCM zasilane sekretem z warstwy
46
+ kwantowo-odpornej (PQC KEM) lub kwantowej dystrybucji klucza (BB84).
47
+
48
+ Przykład:
49
+ cipher = AEAD(key=shared_secret)
50
+ blob = cipher.encrypt(b"tajna wiadomość")
51
+ plaintext = cipher.decrypt(blob)
52
+ """
53
+
54
+ def __init__(self, key: bytes, *, info: bytes = b"qcrypt-aead-v1"):
55
+ """
56
+ Args:
57
+ key: surowy sekret (dowolna długość >= 16 bajtów), np. z KEM lub BB84.
58
+ info: kontekst domenowy dla HKDF (izoluje różne zastosowania klucza).
59
+ """
60
+ if len(key) < 16:
61
+ raise ValueError("Klucz wejściowy musi mieć co najmniej 16 bajtów entropii.")
62
+ self._key = self._derive_key(key, info)
63
+ self._aesgcm = AESGCM(self._key)
64
+
65
+ @staticmethod
66
+ def _derive_key(raw_secret: bytes, info: bytes) -> bytes:
67
+ hkdf = HKDF(
68
+ algorithm=hashes.SHA256(),
69
+ length=KEY_SIZE,
70
+ salt=None,
71
+ info=info,
72
+ )
73
+ return hkdf.derive(raw_secret)
74
+
75
+ def encrypt(self, plaintext: bytes, *, associated_data: bytes | None = None) -> bytes:
76
+ """Szyfruje plaintext, zwraca gotowy blob (nonce + ciphertext+tag)."""
77
+ nonce = os.urandom(NONCE_SIZE)
78
+ ciphertext = self._aesgcm.encrypt(nonce, plaintext, associated_data)
79
+ return EncryptedMessage(nonce=nonce, ciphertext=ciphertext).to_bytes()
80
+
81
+ def decrypt(self, blob: bytes, *, associated_data: bytes | None = None) -> bytes:
82
+ """Odszyfrowuje blob utworzony przez encrypt(). Rzuca DecryptionError
83
+ przy nieprawidłowym kluczu, uszkodzonych danych lub manipulacji."""
84
+ msg = EncryptedMessage.from_bytes(blob)
85
+ try:
86
+ return self._aesgcm.decrypt(msg.nonce, msg.ciphertext, associated_data)
87
+ except Exception as exc: # cryptography rzuca InvalidTag
88
+ raise DecryptionError(
89
+ "Nie udało się odszyfrować — zły klucz, uszkodzone dane lub próba manipulacji."
90
+ ) from exc
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: quant-bvz-secure
3
+ Version: 0.1.0
4
+ Summary: Biblioteka kryptograficzna: post-kwantowa wymiana kluczy (Kyber/Dilithium) + symulacja QKD (BB84) + AEAD
5
+ License: MIT
6
+ Project-URL: Repository, https://github.com/Jokovexor/quant-bvz-secure
7
+ Keywords: post-quantum,pqc,kyber,dilithium,qkd,bb84,cryptography
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Security :: Cryptography
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: cryptography>=42.0
20
+ Provides-Extra: pqc
21
+ Requires-Dist: liboqs-python>=0.10.0; extra == "pqc"
22
+ Provides-Extra: qkd-hw
23
+ Requires-Dist: qiskit>=1.0; extra == "qkd-hw"
24
+ Requires-Dist: qiskit-aer>=0.14; extra == "qkd-hw"
25
+ Provides-Extra: all
26
+ Requires-Dist: liboqs-python>=0.10.0; extra == "all"
27
+ Requires-Dist: qiskit>=1.0; extra == "all"
28
+ Requires-Dist: qiskit-aer>=0.14; extra == "all"
29
+ Dynamic: license-file
30
+
31
+ # qcrypt
32
+
33
+ Biblioteka kryptograficzna łącząca **kryptografię post-kwantową (PQC)**
34
+ z **symulacją kwantowej dystrybucji klucza (QKD / BB84)**.
35
+
36
+ ## Ważne zastrzeżenie
37
+
38
+ Czysty Python nie ma dostępu do sprzętu kwantowego. Ta biblioteka dzieli się
39
+ na dwie realistyczne części:
40
+
41
+ - **`qcrypt.pqc`** — prawdziwe, produkcyjne algorytmy odporne na ataki
42
+ komputerów kwantowych (Kyber/ML-KEM, Dilithium/ML-DSA), działające na
43
+ zwykłym procesorze *już dziś*. To jest realne zastosowanie.
44
+ - **`qcrypt.qkd`** — **symulacja matematyczna** protokołu BB84 do celów
45
+ edukacyjnych. Domyślnie nie wykonuje żadnych operacji na prawdziwych
46
+ kubitach. Można podłączyć `QiskitBackend`, żeby faktycznie budować i
47
+ wykonywać obwody kwantowe na symulatorze Qiskit Aer (lub realnym QPU IBM).
48
+
49
+ ## Instalacja
50
+
51
+ ```bash
52
+ pip install -e . # rdzeń (AEAD, symulacja BB84, X25519)
53
+ pip install -e .[pqc] # + Kyber/Dilithium (liboqs-python)
54
+ pip install -e .[qkd-hw] # + prawdziwe obwody kwantowe (Qiskit)
55
+ pip install -e .[all] # wszystko
56
+ ```
57
+
58
+ `liboqs-python` wymaga skompilowania biblioteki C `liboqs` — zobacz
59
+ https://github.com/open-quantum-safe/liboqs-python
60
+
61
+ ## Szybki start
62
+
63
+ ### 1. Hybrydowa wymiana klucza (produkcyjna)
64
+
65
+ ```python
66
+ from qcrypt import HybridKEM, AEAD
67
+
68
+ bob = HybridKEM()
69
+ bundle = bob.generate_keypair() # Bob -> wysyła do Alice
70
+
71
+ ciphertext, alice_secret = HybridKEM.encapsulate(bundle) # Alice
72
+ bob_secret = bob.decapsulate(ciphertext) # Bob
73
+
74
+ assert alice_secret == bob_secret
75
+
76
+ cipher = AEAD(key=alice_secret)
77
+ blob = cipher.encrypt(b"tajna wiadomosc")
78
+ plaintext = AEAD(key=bob_secret).decrypt(blob)
79
+ ```
80
+
81
+ ### 2. Symulacja BB84 (edukacyjna)
82
+
83
+ ```python
84
+ from qcrypt.qkd.bb84 import BB84Protocol
85
+ from qcrypt.qkd.channel import QuantumChannel
86
+
87
+ # Kanał bez podsłuchu
88
+ channel = QuantumChannel(noise_level=0.02)
89
+ result = BB84Protocol(channel=channel, n_qubits=2048).run()
90
+ print(result.qber, result.eavesdropper_detected)
91
+
92
+ # Kanał z podsłuchem — QBER wzrośnie i zostanie wykryty
93
+ channel_eve = QuantumChannel(eavesdropper=True)
94
+ result_eve = BB84Protocol(channel=channel_eve, n_qubits=2048).run()
95
+ assert result_eve.eavesdropper_detected
96
+ ```
97
+
98
+ ### 3. CLI
99
+
100
+ ```bash
101
+ python -m qcrypt.cli bb84-demo --qubits 4096
102
+ python -m qcrypt.cli bb84-demo --qubits 4096 --eve # symulacja podsłuchu
103
+ python -m qcrypt.cli hybrid-demo --classical-only # bez liboqs
104
+ python -m qcrypt.cli encrypt --key-hex <64 hex znaki> --in plik.txt --out plik.enc
105
+ python -m qcrypt.cli decrypt --key-hex <64 hex znaki> --in plik.enc --out plik.txt
106
+ ```
107
+
108
+ ## Struktura
109
+
110
+ ```
111
+ qcrypt/
112
+ ├── pqc/
113
+ │ ├── kem.py # Kyber/ML-KEM przez liboqs-python
114
+ │ ├── signatures.py # Dilithium/ML-DSA przez liboqs-python
115
+ │ └── hybrid.py # X25519 + Kyber połączone przez HKDF
116
+ ├── qkd/
117
+ │ ├── bb84.py # orkiestracja protokołu, sifting, QBER, privacy amplification
118
+ │ ├── channel.py # model kanału + symulowany podsłuchujący (Eve)
119
+ │ └── backend.py # SimulatedBackend (domyślny) / QiskitBackend (opcjonalny)
120
+ ├── symmetric/
121
+ │ └── aead.py # AES-256-GCM + HKDF do wyprowadzania klucza
122
+ ├── keystore.py # zapis/odczyt kluczy do JSON (base64)
123
+ ├── exceptions.py
124
+ └── cli.py
125
+ ```
126
+
127
+ ## Bezpieczeństwo
128
+
129
+ - Implementacje PQC pochodzą z zaudytowanej biblioteki `liboqs` — nie
130
+ implementujemy własnej kryptografii kratowej.
131
+ - `HybridKEM` łączy X25519 z Kyber: złamanie musi dotyczyć **obu**
132
+ algorytmów jednocześnie, zgodnie z zaleceniami NIST na okres przejściowy.
133
+ - Moduł `qkd` jest **edukacyjny**. Nie używaj go jako jedynej warstwy
134
+ bezpieczeństwa w systemie produkcyjnym bez realnego sprzętu kwantowego i
135
+ zaufanego kanału uwierzytelniania klasycznego (BB84 wymaga uwierzytelnionego
136
+ kanału klasycznego do sifting, inaczej podatny na man-in-the-middle).
@@ -0,0 +1,19 @@
1
+ qcrypt/__init__.py,sha256=trAcbXIhmfRa124wwQaChqKVU5IICpAQU-FZRe3sbC8,962
2
+ qcrypt/cli.py,sha256=bVCuX5F4dMPfOqTUjGVphwlM6JM0NYDgPVpZibZe38E,4497
3
+ qcrypt/exceptions.py,sha256=cJunqBIomK84dZgw5dXBizifG-CRx-Aufb2QMuEIWHE,733
4
+ qcrypt/keystore.py,sha256=KBO0geIkIE1bmBPE7wLRzXY3bRXGEqd6cHjIOliCT7c,4521
5
+ qcrypt/pqc/__init__.py,sha256=bpBxk7zeBuPan6uu7YcupMIXWo9XQSNYcPrCdFEDiYA,214
6
+ qcrypt/pqc/hybrid.py,sha256=yrIcWvHkAt0X_150L3yWERaY-CRgSQ1r0vYbAcQ4sbo,6254
7
+ qcrypt/pqc/kem.py,sha256=i8OXpOSBNihVtaziXsHIzTXqLuUobaoF8NN_p-E0tfQ,3384
8
+ qcrypt/pqc/signatures.py,sha256=rpqhX2TeGYb8sYpd6FuiHGxGJGmKN62GrQGqXXXj6YE,1882
9
+ qcrypt/qkd/__init__.py,sha256=HidmyhUE5uMWLf3UYQtifbe3Ipfv-Ov27XbnBuiyFkY,159
10
+ qcrypt/qkd/backend.py,sha256=Jsxto3cGZ94hTAY_omkAwfunSE06Fg2hOzW05ckseQs,3226
11
+ qcrypt/qkd/bb84.py,sha256=3nDMOuwwz9tiQH0ZhB5hvQEEsHnzakANBBr3zrfskCA,6353
12
+ qcrypt/qkd/channel.py,sha256=DX9cd7o26m80v1aFm2y0-jFlBvDLL4t-zMVVCUtznBo,3119
13
+ qcrypt/symmetric/__init__.py,sha256=SSFlkrD8IsOqFq1YsIDBOqvAnsCEElN13pKVvLd17To,59
14
+ qcrypt/symmetric/aead.py,sha256=bJshMRsjkdX3Ef8u5Jwf1x9ej02pf-dqvaghChdll1k,3388
15
+ quant_bvz_secure-0.1.0.dist-info/licenses/LICENSE,sha256=YDTqs06lZtO7a0hSms2_Iy4ZC94pvwpOmBlY5AInOes,1073
16
+ quant_bvz_secure-0.1.0.dist-info/METADATA,sha256=ewT-x8eF7GUp-sizhCCWrGpR7SkjGihmlzRZZMbXsoQ,5202
17
+ quant_bvz_secure-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
18
+ quant_bvz_secure-0.1.0.dist-info/top_level.txt,sha256=wnyBACCQ9zO8m1BZEqJhxa0DNPKlCejkYApFsQ5RtTw,7
19
+ quant_bvz_secure-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bartosz Radomski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ qcrypt