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 +27 -0
- qcrypt/cli.py +118 -0
- qcrypt/exceptions.py +22 -0
- qcrypt/keystore.py +123 -0
- qcrypt/pqc/__init__.py +5 -0
- qcrypt/pqc/hybrid.py +155 -0
- qcrypt/pqc/kem.py +96 -0
- qcrypt/pqc/signatures.py +58 -0
- qcrypt/qkd/__init__.py +4 -0
- qcrypt/qkd/backend.py +88 -0
- qcrypt/qkd/bb84.py +160 -0
- qcrypt/qkd/channel.py +80 -0
- qcrypt/symmetric/__init__.py +3 -0
- qcrypt/symmetric/aead.py +90 -0
- quant_bvz_secure-0.1.0.dist-info/METADATA +136 -0
- quant_bvz_secure-0.1.0.dist-info/RECORD +19 -0
- quant_bvz_secure-0.1.0.dist-info/WHEEL +5 -0
- quant_bvz_secure-0.1.0.dist-info/licenses/LICENSE +21 -0
- quant_bvz_secure-0.1.0.dist-info/top_level.txt +1 -0
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
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)
|
qcrypt/pqc/signatures.py
ADDED
|
@@ -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
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)
|
qcrypt/symmetric/aead.py
ADDED
|
@@ -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,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
|