Skip to content

Commit ce99439

Browse files
committed
refactor: smp encryption switch to xchacha20poly1305
1 parent 77f6788 commit ce99439

File tree

4 files changed

+44
-65
lines changed

4 files changed

+44
-65
lines changed

core/constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# crypto parameters (bytes)
1313
CHALLENGE_LEN = 11264
1414

15-
CHACHA20POLY1305_NONCE_LEN = 12
15+
XCHACHA20POLY1305_NONCE_LEN = 24
1616

1717
OTP_PAD_SIZE = 11264
1818
OTP_PADDING_LENGTH = 2
@@ -71,5 +71,5 @@
7171
ARGON2_MEMORY = 256 * 1024 # MB
7272
ARGON2_ITERS = 3
7373
ARGON2_OUTPUT_LEN = 32 # bytes
74-
ARGON2_SALT_LEN = 32 # bytes
74+
ARGON2_SALT_LEN = 16 # bytes (Must be always 16 for interoperability with libsodium.)
7575
ARGON2_LANES = 4

core/trad_crypto.py

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,10 @@
88
These functions rely on the cryptography library and are intended for use within Coldwire's higher-level protocol logic.
99
"""
1010

11-
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
12-
from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
13-
from cryptography.hazmat.primitives import hashes
14-
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
11+
from nacl import pwhash, bindings
1512
from core.constants import (
1613
OTP_PAD_SIZE,
17-
CHACHA20POLY1305_NONCE_LEN,
14+
XCHACHA20POLY1305_NONCE_LEN,
1815
ARGON2_ITERS,
1916
ARGON2_MEMORY,
2017
ARGON2_LANES,
@@ -41,15 +38,7 @@ def sha3_512(data: bytes) -> bytes:
4138
return h.digest()
4239

4340

44-
def hkdf(key: bytes, length: int = 32, salt: bytes = None, info: bytes = None) -> bytes:
45-
return HKDF(
46-
algorithm = hashes.SHA3_256(),
47-
length = length,
48-
salt = salt,
49-
info = info,
50-
).derive(key)
51-
52-
def derive_key_argon2id(password: bytes, salt: bytes = None, salt_length: int = ARGON2_SALT_LEN, output_length: int = ARGON2_OUTPUT_LEN) -> tuple[bytes, bytes]:
41+
def derive_key_argon2id(password: bytes, salt: bytes = None, output_length: int = ARGON2_OUTPUT_LEN) -> tuple[bytes, bytes]:
5342
"""
5443
Derive a symmetric key from a password using Argon2id.
5544
@@ -67,20 +56,18 @@ def derive_key_argon2id(password: bytes, salt: bytes = None, salt_length: int =
6756
- salt: The salt used for derivation.
6857
"""
6958
if salt is None:
70-
salt = secrets.token_bytes(salt_length)
59+
salt = secrets.token_bytes(ARGON2_SALT_LEN)
7160

72-
kdf = Argon2id(
73-
salt=salt,
74-
iterations=ARGON2_ITERS,
75-
memory_cost=ARGON2_MEMORY,
76-
length=output_length,
77-
lanes=ARGON2_LANES
78-
)
79-
derived_key = kdf.derive(password)
80-
return derived_key, salt
61+
return pwhash.argon2id.kdf(
62+
output_length,
63+
password,
64+
salt,
65+
opslimit = ARGON2_ITERS,
66+
memlimit = ARGON2_MEMORY
67+
), salt
8168

8269

83-
def encrypt_chacha20poly1305(key: bytes, plaintext: bytes, counter: int = None, counter_safety: int = 2 ** 32) -> tuple[bytes, bytes]:
70+
def encrypt_xchacha20poly1305(key: bytes, plaintext: bytes, counter: int = None, counter_safety: int = 2 ** 32) -> tuple[bytes, bytes]:
8471
"""
8572
Encrypt plaintext using ChaCha20Poly1305.
8673
@@ -96,19 +83,19 @@ def encrypt_chacha20poly1305(key: bytes, plaintext: bytes, counter: int = None,
9683
- nonce: The randomly generated AES-GCM nonce.
9784
- ciphertext: The encrypted data including the authentication tag.
9885
"""
99-
nonce = secrets.token_bytes(CHACHA20POLY1305_NONCE_LEN)
86+
nonce = secrets.token_bytes(XCHACHA20POLY1305_NONCE_LEN)
10087
if counter is not None:
10188
if counter > counter_safety:
10289
raise ValueError("ChaCha counter has overflowen")
10390

104-
nonce = nonce[:CHACHA20POLY1305_NONCE_LEN - 4] + counter.to_bytes(4, "big")
91+
nonce = nonce[:XCHACHA20POLY1305_NONCE_LEN - 4] + counter.to_bytes(4, "big")
92+
93+
ciphertext = bindings.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, None, nonce, key)
10594

106-
chacha = ChaCha20Poly1305(key)
107-
ciphertext = chacha.encrypt(nonce, plaintext, None)
10895
return nonce, ciphertext
10996

11097

111-
def decrypt_chacha20poly1305(key: bytes, nonce: bytes, ciphertext: bytes) -> bytes:
98+
def decrypt_xchacha20poly1305(key: bytes, nonce: bytes, ciphertext: bytes) -> bytes:
11299
"""
113100
Decrypt ciphertext using ChaCha20Poly1305.
114101
@@ -122,7 +109,7 @@ def decrypt_chacha20poly1305(key: bytes, nonce: bytes, ciphertext: bytes) -> byt
122109
Returns:
123110
The decrypted plaintext bytes.
124111
"""
125-
chacha = ChaCha20Poly1305(key)
126-
return chacha.decrypt(nonce, ciphertext, None)
112+
113+
return bindings.crypto_aead_xchacha20poly1305_ietf_decrypt(ciphertext, None, nonce, key)
127114

128115

logic/smp.py

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,20 @@
4646
from core.trad_crypto import (
4747
derive_key_argon2id,
4848
sha3_512,
49-
hkdf,
50-
encrypt_chacha20poly1305,
51-
decrypt_chacha20poly1305
49+
encrypt_xchacha20poly1305,
50+
decrypt_xchacha20poly1305
5251
)
5352
from base64 import b64encode, b64decode
5453
from core.constants import (
5554
SMP_NONCE_LENGTH,
5655
SMP_PROOF_LENGTH,
5756
SMP_QUESTION_MAX_LEN,
5857
SMP_ANSWER_OUTPUT_LEN,
58+
ARGON2_SALT_LEN,
5959
ML_KEM_1024_NAME,
6060
ML_KEM_1024_CT_LEN,
6161
ML_DSA_87_PK_LEN,
62-
CHACHA20POLY1305_NONCE_LEN
62+
XCHACHA20POLY1305_NONCE_LEN
6363
)
6464
import hashlib
6565
import secrets
@@ -134,13 +134,9 @@ def smp_step_2(user_data: dict, user_data_lock, contact_id: str, message: dict,
134134
our_nonce = secrets.token_bytes(SMP_NONCE_LENGTH)
135135

136136
key_ciphertext, chacha_key = encap_shared_secret(contact_kem_public_key, ML_KEM_1024_NAME)
137-
chacha_key = hkdf(
138-
chacha_key,
139-
salt = contact_id.encode("utf-8") + our_id.encode("utf-8"),
140-
info = b"Coldwire SMP encryption ChaCha20 key"
141-
)
137+
chacha_key = sha3_512(chacha_key)[:32]
142138

143-
ciphertext_nonce, ciphertext_blob = encrypt_chacha20poly1305(
139+
ciphertext_nonce, ciphertext_blob = encrypt_xchacha20poly1305(
144140
chacha_key,
145141
signing_public_key + our_nonce,
146142
counter = 2
@@ -187,19 +183,14 @@ def smp_step_3(user_data: dict, user_data_lock: threading.Lock, contact_id: str,
187183
ciphertext_blob = b64decode(message["ciphertext_blob"], validate = True)
188184
key_ciphertext = ciphertext_blob[:ML_KEM_1024_CT_LEN]
189185

190-
print(len(our_kem_private_key))
191186
chacha_key = decap_shared_secret(key_ciphertext, our_kem_private_key, ML_KEM_1024_NAME)
192187

193-
chacha_key = hkdf(
194-
chacha_key,
195-
salt = our_id.encode("utf-8") + contact_id.encode("utf-8"),
196-
info = b"Coldwire SMP encryption ChaCha20 key"
197-
)
188+
chacha_key = sha3_512(chacha_key)[:32]
198189

199-
smp_plaintext = decrypt_chacha20poly1305(
190+
smp_plaintext = decrypt_xchacha20poly1305(
200191
chacha_key,
201-
ciphertext_blob[ML_KEM_1024_CT_LEN : ML_KEM_1024_CT_LEN + CHACHA20POLY1305_NONCE_LEN],
202-
ciphertext_blob[ML_KEM_1024_CT_LEN + CHACHA20POLY1305_NONCE_LEN:]
192+
ciphertext_blob[ML_KEM_1024_CT_LEN : ML_KEM_1024_CT_LEN + XCHACHA20POLY1305_NONCE_LEN],
193+
ciphertext_blob[ML_KEM_1024_CT_LEN + XCHACHA20POLY1305_NONCE_LEN:]
203194
)
204195

205196
contact_signing_public_key = smp_plaintext[:ML_DSA_87_PK_LEN]
@@ -212,7 +203,7 @@ def smp_step_3(user_data: dict, user_data_lock: threading.Lock, contact_id: str,
212203
contact_key_fingerprint = sha3_512(contact_signing_public_key)
213204

214205
# Derieve a high-entropy secret key from the low-entropy answer
215-
argon2id_salt = sha3_512(contact_nonce + our_nonce)
206+
argon2id_salt = sha3_512(contact_nonce + our_nonce)[:ARGON2_SALT_LEN]
216207
answer_secret, _ = derive_key_argon2id(answer.encode("utf-8"), salt = argon2id_salt, output_length = SMP_ANSWER_OUTPUT_LEN)
217208

218209
# Compute our proof
@@ -221,7 +212,7 @@ def smp_step_3(user_data: dict, user_data_lock: threading.Lock, contact_id: str,
221212

222213
logger.debug("Our proof of contact (%s) public-key fingerprint: %s", contact_id, our_proof)
223214

224-
ciphertext_nonce, ciphertext_blob = encrypt_chacha20poly1305(
215+
ciphertext_nonce, ciphertext_blob = encrypt_xchacha20poly1305(
225216
chacha_key,
226217
signing_public_key + our_nonce + our_proof + question.encode("utf-8"),
227218
counter = 3
@@ -259,7 +250,7 @@ def smp_step_4_request_answer(user_data, user_data_lock, contact_id, message, ui
259250
tmp_key = b64decode(user_data["contacts"][contact_id]["lt_sign_key_smp"]["tmp_key"])
260251

261252
ciphertext_blob = b64decode(message["ciphertext_blob"], validate = True)
262-
smp_plaintext = decrypt_chacha20poly1305(tmp_key, ciphertext_blob[:CHACHA20POLY1305_NONCE_LEN], ciphertext_blob[CHACHA20POLY1305_NONCE_LEN:])
253+
smp_plaintext = decrypt_xchacha20poly1305(tmp_key, ciphertext_blob[:XCHACHA20POLY1305_NONCE_LEN], ciphertext_blob[XCHACHA20POLY1305_NONCE_LEN:])
263254

264255
contact_signing_public_key = smp_plaintext[:ML_DSA_87_PK_LEN]
265256
contact_nonce = b64encode(smp_plaintext[ML_DSA_87_PK_LEN : SMP_NONCE_LENGTH + ML_DSA_87_PK_LEN]).decode()
@@ -306,7 +297,7 @@ def smp_step_4_answer_provided(user_data, user_data_lock, contact_id, answer, ui
306297
our_key_fingerprint = sha3_512(our_signing_public_key)
307298

308299
# Derieve a high-entropy secret key from the low-entropy answer
309-
argon2id_salt = sha3_512(our_nonce + contact_nonce)
300+
argon2id_salt = sha3_512(our_nonce + contact_nonce)[:ARGON2_SALT_LEN]
310301
answer_secret, _ = derive_key_argon2id(answer.encode("utf-8"), salt = argon2id_salt, output_length = SMP_ANSWER_OUTPUT_LEN)
311302

312303
# Compute our proof
@@ -330,7 +321,7 @@ def smp_step_4_answer_provided(user_data, user_data_lock, contact_id, answer, ui
330321
our_proof = contact_nonce + our_nonce + contact_key_fingerprint
331322
our_proof = hmac.new(answer_secret, our_proof, hashlib.sha3_512).digest()
332323

333-
ciphertext_nonce, ciphertext_blob = encrypt_chacha20poly1305(
324+
ciphertext_nonce, ciphertext_blob = encrypt_xchacha20poly1305(
334325
tmp_key,
335326
our_proof,
336327
counter = 4
@@ -377,15 +368,15 @@ def smp_step_5(user_data, user_data_lock, contact_id, message, ui_queue) -> None
377368
our_key_fingerprint = sha3_512(our_signing_public_key + our_kem_public_key)
378369

379370
# Derieve a high-entropy secret key from the low-entropy answer
380-
argon2id_salt = sha3_512(contact_nonce + our_nonce)
371+
argon2id_salt = sha3_512(contact_nonce + our_nonce)[:ARGON2_SALT_LEN]
381372
answer_secret, _ = derive_key_argon2id(answer.encode("utf-8"), salt = argon2id_salt, output_length = SMP_ANSWER_OUTPUT_LEN)
382373

383374
# Compute the proof
384375
our_proof = our_nonce + contact_nonce + our_key_fingerprint
385376
our_proof = hmac.new(answer_secret, our_proof, hashlib.sha3_512).digest()
386377

387378
ciphertext_blob = b64decode(message["ciphertext_blob"], validate = True)
388-
contact_proof = decrypt_chacha20poly1305(tmp_key, ciphertext_blob[:CHACHA20POLY1305_NONCE_LEN], ciphertext_blob[CHACHA20POLY1305_NONCE_LEN:])
379+
contact_proof = decrypt_xchacha20poly1305(tmp_key, ciphertext_blob[:XCHACHA20POLY1305_NONCE_LEN], ciphertext_blob[XCHACHA20POLY1305_NONCE_LEN:])
389380

390381

391382
logger.debug("SMP Proof sent to us: %s", contact_proof)

logic/storage.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from core.constants import (
44
ML_KEM_1024_NAME,
55
CLASSIC_MCELIECE_8_F_NAME,
6-
ACCOUNT_FILE_PATH
6+
ACCOUNT_FILE_PATH,
7+
ARGON2_SALT_LEN
78

89
)
910
import core.trad_crypto as crypto
@@ -29,11 +30,11 @@ def load_account_data(password = None) -> dict:
2930

3031
# first 12 bytes is nonce, and last 32 bytes is the password salt,
3132
# and the ciphertext is inbetween.
32-
password_kdf, _ = crypto.derive_key_argon2id(password.encode(), salt=blob[-32:])
33+
password_kdf, _ = crypto.derive_key_argon2id(password.encode(), salt=blob[-ARGON2_SALT_LEN:])
3334

34-
blob = blob[:-32]
35+
blob = blob[:-ARGON2_SALT_LEN]
3536

36-
user_data = json.loads(crypto.decrypt_chacha20poly1305(password_kdf, blob[:12], blob[12:]))
37+
user_data = json.loads(crypto.decrypt_xchacha20poly1305(password_kdf, blob[:12], blob[12:]))
3738

3839

3940

@@ -195,7 +196,7 @@ def save_account_data(user_data: dict, user_data_lock, password = None) -> None:
195196
password_kdf, password_salt = crypto.derive_key_argon2id(password.encode())
196197

197198

198-
nonce, ciphertext = crypto.encrypt_chacha20poly1305(password_kdf, json.dumps(user_data).encode("utf-8"))
199+
nonce, ciphertext = crypto.encrypt_xchacha20poly1305(password_kdf, json.dumps(user_data).encode("utf-8"))
199200
with open(ACCOUNT_FILE_PATH, "wb") as f:
200201
f.write(nonce + ciphertext + password_salt)
201202

0 commit comments

Comments
 (0)