Skip to content

Commit af17dfa

Browse files
committed
docs: Cleanup code, and add documentation
1 parent 9fbe3b9 commit af17dfa

File tree

5 files changed

+214
-96
lines changed

5 files changed

+214
-96
lines changed

core/constants.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# app metadata
2-
VERSION = "0.1"
32
APP_NAME = "Coldwire"
3+
APP_VERSION = "0.1"
44

55
# network defaults (seconds)
66
LONGPOLL_MIN = 5
@@ -14,9 +14,11 @@
1414
OTP_PADDING_LIMIT = 1024
1515

1616
# NIST-specified key sizes (bytes) and metadata
17+
ML_KEM_1024_NAME = "Kyber1024"
1718
ML_KEM_1024_SK_LEN = 3168
1819
ML_KEM_1024_PK_LEN = 1568
1920

21+
2022
ML_DSA_87_NAME = "Dilithium5"
2123
ML_DSA_87_SK_LEN = 4864
2224
ML_DSA_87_PK_LEN = 2592

core/crypto.py

Lines changed: 122 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,140 +1,177 @@
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+
219
import oqs
320
import 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

534
def 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

949
def 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-
2080
def 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-
62105
def 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-
91122
def 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-
114154
def 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

144181
def 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-
173203
def 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

Comments
 (0)