1- from core .constants import *
1+ """
2+ core/crypto
3+ -----------
4+ Post-quantum cryptographic operations for Coldwire.
5+
6+ Implements:
7+ - Key generation (ML-KEM-1024 / Kyber, ML-DSA-87 / Dilithium5)
8+ - Signature creation and verification
9+ - One-Time Pad (OTP) encryption with padding
10+ - Kyber-based OTP key exchange
11+ - Secure random number generation
12+
13+ Notes:
14+ - Kyber keys and ciphertext sizes follow NIST spec for ML-KEM-1024.
15+ - Dilithium5 keys/signature sizes follow NIST spec for ML-DSA-87.
16+ - OTP padding randomizes message lengths to resist traffic analysis.
17+ """
18+
219import oqs
320import secrets
21+ from core .constants import (
22+ OTP_PAD_SIZE ,
23+ OTP_PADDING_LIMIT ,
24+ OTP_PADDING_LENGTH ,
25+ ML_KEM_1024_NAME ,
26+ ML_KEM_1024_SK_LEN ,
27+ ML_KEM_1024_PK_LEN ,
28+ ML_DSA_87_NAME ,
29+ ML_DSA_87_SK_LEN ,
30+ ML_DSA_87_PK_LEN ,
31+ ML_DSA_87_SIGN_LEN
32+ )
433
534def create_signature (algorithm : str , message : bytes , private_key : bytes ) -> bytes :
35+ """
36+ Creates a digital signature for a message using a post-quantum signature scheme.
37+
38+ Args:
39+ algorithm: PQ signature algorithm (e.g. "Dilithium5").
40+ message: Data to sign.
41+ private_key: Private key bytes.
42+
43+ Returns:
44+ Signature bytes of fixed size defined by the algorithm.
45+ """
646 with oqs .Signature (algorithm , secret_key = private_key ) as signer :
7- return signer .sign (message )
47+ return signer .sign (message )
848
949def verify_signature (algorithm : str , message : bytes , signature : bytes , public_key : bytes ) -> bool :
50+ """
51+ Verifies a post-quantum signature.
52+
53+ Args:
54+ algorithm: PQ signature algorithm (e.g. "Dilithium5").
55+ message: Original message data.
56+ signature: Signature to verify.
57+ public_key: Corresponding public key bytes.
58+
59+ Returns:
60+ True if valid, False if invalid.
61+ """
1062 with oqs .Signature (algorithm ) as verifier :
1163 return verifier .verify (message , signature , public_key )
1264
13- def generate_sign_keys (algorithm : str = "Dilithium5" ):
65+ def generate_sign_keys (algorithm : str = ML_DSA_87_NAME ):
66+ """
67+ Generates a new post-quantum signature keypair.
68+
69+ Args:
70+ algorithm: PQ signature algorithm (default ML-DSA-87 / Dilithium5).
71+
72+ Returns:
73+ (private_key, public_key) as bytes.
74+ """
1475 with oqs .Signature (algorithm ) as signer :
1576 public_key = signer .generate_keypair ()
1677 private_key = signer .export_secret_key ()
1778 return private_key , public_key
1879
19-
2080def otp_encrypt_with_padding (plaintext : bytes , key : bytes , padding_limit : int ) -> bytes :
2181 """
22- Encrypts a plaintext with a one-time pad with padding.
23-
24- - Always prefixes the plaintext with a OTP_PADDING_LENGTH big-endian length.
25- - Pads with random bytes up to padding_limit, if padding_limit is 0
26- - Then no padding is added
82+ Encrypts plaintext using a one-time pad with random padding.
2783
28- Args :
29- plaintext: The plaintext message to encrypt .
30- key: The one-time pad. Must be at least as long as the plaintext block .
31- padding_limit: The padding limit
84+ Process :
85+ - Prefixes length of padding .
86+ - Adds random padding (0..padding_limit bytes) .
87+ - XORs with one-time pad key.
3288
33- Returns:
34- Ciphertext as bytes.
89+ Args:
90+ plaintext: Data to encrypt.
91+ key: OTP key (≥ plaintext length + padding).
92+ padding_limit: Max padding length.
3593
94+ Returns:
95+ Ciphertext bytes.
3696 """
37-
3897 if padding_limit > ((2 ** (8 * OTP_PADDING_LENGTH )) - 1 ):
3998 raise ValueError ("Padding too large" )
4099
41- # NOTE: If padding_limit is 0, the plaintext_paddding_bytes would also be 0
42- # Which means an attacker could potentially learn some info the first 2 bytes
43- # which consists of the padding_length. This is fine as OTP security guarantees
44- # as long as the rest of key is random and never reused, the security
45- # wouldn't be affected at all.
46- #
47- # However, this could aid the adversary to confidently recover message length
48- # as he now knows that the padding is 0 and hence could calculate the length
49- # of the plaintext.
50- # This can be prevented if padding_limit is randomized even just slightly
51- # We leave that responsibility to the caller
52-
53100 plaintext_padding = secrets .token_bytes (padding_limit )
54101 padding_length_bytes = len (plaintext_padding ).to_bytes (OTP_PADDING_LENGTH , "big" )
55-
56102 padded_plaintext = padding_length_bytes + plaintext + plaintext_padding
57-
58103 return one_time_pad (padded_plaintext , key )
59104
60-
61-
62105def otp_decrypt_with_padding (ciphertext : bytes , key : bytes ) -> bytes :
63106 """
64- Decrypts a one-time- pad ciphertext that has been padded
107+ Decrypts one-time pad ciphertext that contains prefixed padding length.
65108
66- Args:
67- ciphertext: The padded ciphertext message to decrypt.
68- key: The one-time pad. Must be at least as long as the ciphertext block.
69-
70- Returns:
71- Plaintext as bytes.
109+ Args:
110+ ciphertext: Ciphertext bytes.
111+ key: OTP key (≥ ciphertext length).
72112
113+ Returns:
114+ Original plaintext bytes without padding.
73115 """
74-
75116 plaintext_with_padding = one_time_pad (ciphertext , key )
76-
77- # Extract the plaintext length
78- padding_length = int .from_bytes (plaintext_with_padding [:OTP_PADDING_LENGTH ], "big" )
79-
80- # Return the plaintext without the padding nor padding length
117+ padding_length = int .from_bytes (plaintext_with_padding [:OTP_PADDING_LENGTH ], "big" )
81118 if padding_length != 0 :
82119 return plaintext_with_padding [OTP_PADDING_LENGTH : - padding_length ]
83-
84- # Return the plaintext without padding_length. Needed because
85- # if padding_length is 0, -padding_length would return an empty string
86-
87120 return plaintext_with_padding [OTP_PADDING_LENGTH :]
88121
89-
90-
91122def one_time_pad (plaintext : bytes , key : bytes ) -> bytes :
92123 """
93- Does one-time-pad XOR encryption on plaintext with key and returns result
124+ XOR-based One-Time Pad encryption/decryption.
125+
126+ Args:
127+ plaintext: Input data.
128+ key: Random key (equal or longer length).
94129
95- OTP is the only known encryption system that is mathematically proven to be unbreakable under the principles of information theory
96- if the key is random and is never reused
130+ Returns:
131+ XORed result (ciphertext or plaintext).
97132 """
98133 otpd_plaintext = b''
99134 for index , plain_byte in enumerate (plaintext ):
100135 key_byte = key [index ]
101136 otpd_plaintext += bytes ([plain_byte ^ key_byte ])
102-
103137 return otpd_plaintext
104138
139+ def generate_kem_keys (algorithm : str = ML_KEM_1024_NAME ):
140+ """
141+ Generates ML-KEM-1024 keypair (Kyber).
105142
143+ Args:
144+ algorithm: PQ KEM algorithm (default Kyber1024).
106145
107- def generate_kem_keys (algorithm : str = "Kyber1024" ):
146+ Returns:
147+ (private_key, public_key) as bytes.
148+ """
108149 with oqs .KeyEncapsulation (algorithm ) as kem :
109150 public_key = kem .generate_keypair ()
110151 private_key = kem .export_secret_key ()
111152 return private_key , public_key
112153
113-
114154def decrypt_kyber_shared_secrets (ciphertext_blob : bytes , private_key : bytes , otp_pad_size : int = OTP_PAD_SIZE ):
115155 """
116- Decapsulates shared_secrets of size otp_pad_size and returns the resulting shared_secrets.
117- The ciphertexts_blob is expected to be a concatenated sequence of Kyber ciphertexts,
118- originally generated by generate_kyber_shared_secrets and sent by the contact.
156+ Decrypts concatenated Kyber ciphertexts to derive shared one-time pad.
119157
120- The shared_secrets are meant to be used as one-time-pads to decrypt received messages.
158+ Args:
159+ ciphertext_blob: Concatenated Kyber ciphertexts.
160+ private_key: ML-KEM-1024 private key.
161+ otp_pad_size: Desired OTP pad size in bytes.
121162
122- Kyber1024 has a defined fixed-size ciphertext of 1568 bytes each, which allows us to safely
123- split the blob and decapsulate in order .
163+ Returns:
164+ Shared secret OTP pad bytes .
124165 """
125-
126166 cipher_size = 1568 # Kyber1024 ciphertext size
127-
128167 shared_secrets = b''
129168 cursor = 0
130169
131- with oqs .KeyEncapsulation ("Kyber1024" , secret_key = private_key ) as kem :
170+ with oqs .KeyEncapsulation (ML_KEM_1024_NAME , secret_key = private_key ) as kem :
132171 while len (shared_secrets ) < otp_pad_size :
133172 ciphertext = ciphertext_blob [cursor :cursor + cipher_size ]
134-
135173 if len (ciphertext ) != cipher_size :
136174 raise ValueError ("Ciphertext blob is malformed or incomplete" )
137-
138175 shared_secret = kem .decap_secret (ciphertext )
139176 shared_secrets += shared_secret
140177 cursor += cipher_size
@@ -143,33 +180,35 @@ def decrypt_kyber_shared_secrets(ciphertext_blob: bytes, private_key: bytes, otp
143180
144181def generate_kyber_shared_secrets (public_key : bytes , otp_pad_size : int = OTP_PAD_SIZE ):
145182 """
146- Generates shared_secrets of size otp_pad_size and returns both the ciphertext list-
147- and the generated shared_secrets.
148- The shared_secrets are meant to be used as one-time-pads to encypt our messages,
149- while the ciphertext is meant to be sent to the contact.
150-
151-
152- Default desired One-time-pad size is set to 10 kilobytes which is meant to accommodate-
153- around 10 messages, assuming every message is 1024 bytes with the padding applied.
183+ Generates a one-time pad via Kyber encapsulation.
154184
185+ Args:
186+ public_key: Recipient's ML-KEM-1024 public key.
187+ otp_pad_size: Desired OTP pad size in bytes.
155188
156- We concatenate the ciphertexts together safely because Kyper1024 has a defined fixed-size ciphertext-
157- of 1568 bytes each .
189+ Returns:
190+ (ciphertexts_blob, shared_secrets) for transport & encryption .
158191 """
159-
160192 shared_secrets = b''
161193 ciphertexts_blob = b''
162194
163- with oqs .KeyEncapsulation ("Kyber1024" ) as kem :
195+ with oqs .KeyEncapsulation (ML_KEM_1024_NAME ) as kem :
164196 while len (shared_secrets ) < otp_pad_size :
165197 ciphertext , shared_secret = kem .encap_secret (public_key )
166-
167198 ciphertexts_blob += ciphertext
168199 shared_secrets += shared_secret
169200
170201 return ciphertexts_blob , shared_secrets [:otp_pad_size ]
171202
172-
173203def random_number_range (a : int , b : int ) -> int :
174- return secrets .randbelow (b - a + 1 ) + a
204+ """
205+ Generates a secure random integer in [a, b].
206+
207+ Args:
208+ a: Minimum value.
209+ b: Maximum value.
175210
211+ Returns:
212+ Secure random integer between a and b inclusive.
213+ """
214+ return secrets .randbelow (b - a + 1 ) + a
0 commit comments