Skip to content

Commit 9808376

Browse files
Create crypto_production.py
1 parent 96a80e0 commit 9808376

1 file changed

Lines changed: 151 additions & 0 deletions

File tree

prototype/crypto_production.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# prototype/crypto_production.py (ENHANCED)
2+
from typing import Optional, Tuple
3+
import os
4+
from cryptography.hazmat.primitives.asymmetric import x25519
5+
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
6+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
7+
from cryptography.hazmat.primitives import hashes
8+
9+
OQS_AVAILABLE = False
10+
try:
11+
import oqs as _oqs
12+
OQS_AVAILABLE = True
13+
except Exception:
14+
pass
15+
16+
17+
class ProductionPQCAdapter:
18+
"""Production-ready PQC adapter with full hybrid KEM support."""
19+
20+
def __init__(self, oqs_alg: str = 'Kyber768'):
21+
self._priv_x25519 = x25519.X25519PrivateKey.generate()
22+
self.pub_x25519 = self._priv_x25519.public_key()
23+
24+
self.oqs_supported = False
25+
self.oqs_alg = oqs_alg
26+
27+
if OQS_AVAILABLE:
28+
try:
29+
kem_cls = getattr(_oqs, 'KeyEncapsulation', None) or \
30+
getattr(_oqs, 'KEM', None)
31+
if kem_cls:
32+
self.kem = kem_cls(oqs_alg)
33+
pub = self.kem.generate_keypair()
34+
if isinstance(pub, tuple):
35+
pub = pub[0]
36+
self.oqs_public = pub
37+
self.oqs_supported = True
38+
except Exception:
39+
pass
40+
41+
def public_bytes(self) -> bytes:
42+
"""Return X25519 public key."""
43+
return self.pub_x25519.public_bytes(
44+
encoding=x25519.Encoding.Raw,
45+
format=x25519.PublicFormat.Raw,
46+
)
47+
48+
def get_oqs_public(self) -> Optional[bytes]:
49+
"""Return OQS public key if available."""
50+
return self.oqs_public if self.oqs_supported else None
51+
52+
def derive_shared(self, peer_public_bytes: bytes) -> bytes:
53+
"""Derive symmetric AEAD key from X25519 DH."""
54+
peer_pub = x25519.X25519PublicKey.from_public_bytes(peer_public_bytes)
55+
shared = self._priv_x25519.exchange(peer_pub)
56+
57+
hkdf = HKDF(
58+
algorithm=hashes.SHA384(), # Use SHA-384 for stronger security
59+
length=48, # 48 bytes: 32 for AEAD key + 16 for nonce IV
60+
salt=b'mohawk-hybrid-key-salt',
61+
info=b'hybrid-key-derivation-v2',
62+
)
63+
64+
return hkdf.derive(shared)
65+
66+
def encap(self, peer_oqs_pub: bytes) -> Tuple[bytes, bytes]:
67+
"""Encapsulate to peer's OQS public key."""
68+
if not self.oqs_supported or not getattr(self, 'kem', None):
69+
raise RuntimeError('OQS not available')
70+
71+
# Use correct API method based on pyOQS version
72+
if hasattr(self.kem, 'encapsulate'):
73+
ct, ss = self.kem.encapsulate(peer_oqs_pub)
74+
elif hasattr(self.kem, 'encap_secret'):
75+
ct, ss = self.kem.encap_secret(peer_oqs_pub)
76+
else:
77+
raise AttributeError('Unsupported OQS encapsulation method')
78+
79+
return ct, ss
80+
81+
def decap(self, ct: bytes) -> bytes:
82+
"""Decapsulate ciphertext to retrieve shared secret."""
83+
if not self.oqs_supported or not getattr(self, 'kem', None):
84+
raise RuntimeError('OQS not available')
85+
86+
if hasattr(self.kem, 'decapsulate'):
87+
ss = self.kem.decapsulate(ct)
88+
elif hasattr(self.kem, 'decap_secret'):
89+
ss = self.kem.decap_secret(ct)
90+
else:
91+
raise AttributeError('Unsupported OQS decapsulation method')
92+
93+
return ss
94+
95+
96+
class ReplayProtectedAEAD:
97+
"""Production AEAD with replay protection."""
98+
99+
def __init__(
100+
self,
101+
key: bytes,
102+
expected_sender_id: str,
103+
nonce_expiry_seconds: int = 3600,
104+
max_nonces_per_window: int = 1000
105+
):
106+
self.key = key
107+
self.sender_id = expected_sender_id
108+
self.nonce_expiry = nonce_expiry_seconds
109+
self.max_nonces = max_nonces_per_window
110+
111+
# Nonce tracking with time windows
112+
self.seen_nonces: Dict[str, Tuple[float, int]] = {} # nonce_hex -> (timestamp, usage_count)
113+
114+
self.aead = ChaCha20Poly1305(key)
115+
116+
def is_nonce_fresh(self, nonce: bytes) -> bool:
117+
"""Check if nonce hasn't been used recently."""
118+
nonce_str = nonce.hex()
119+
120+
if nonce_str in self.seen_nonces:
121+
last_seen, usage_count = self.seen_nonces[nonce_str]
122+
time_diff = time.time() - last_seen
123+
124+
# Check if within expiry window
125+
if time_diff < self.nonce_expiry:
126+
# Count usages in current window
127+
if usage_count >= self.max_nonces or time_diff > 300: # 5 min refresh
128+
return False
129+
else:
130+
# First time seeing this nonce
131+
self.seen_nonces[nonce_str] = (time.time(), 1)
132+
133+
return True
134+
135+
def encrypt(self, plaintext: bytes, aad: bytes = b'') -> Tuple[bytes, bytes]:
136+
"""Encrypt with replay protection."""
137+
nonce = os.urandom(12)
138+
139+
# Check for replay before encryption
140+
if not self.is_nonce_fresh(nonce):
141+
raise ReplayError(f"Nonce {nonce.hex()} is stale")
142+
143+
nonce, ct = self.aead.encrypt(nonce, plaintext, aad)
144+
return nonce, ct
145+
146+
def decrypt(self, nonce: bytes, ciphertext: bytes, aad: bytes = b'') -> bytes:
147+
"""Decrypt with replay protection."""
148+
if not self.is_nonce_fresh(nonce):
149+
raise ReplayError(f"Nonce {nonce.hex()} is stale")
150+
151+
return self.aead.decrypt(nonce, ciphertext, aad)

0 commit comments

Comments
 (0)