Coverage for app / backend / src / tests / test_crypto.py: 99%
129 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-03 06:18 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-03 06:18 +0000
1import binascii
3import nacl.utils
4import pytest
5from nacl.exceptions import CryptoError
6from nacl.exceptions import TypeError as NaClTypeError
8from couchers import crypto
9from couchers.proto.internal import internal_pb2
10from couchers.utils import Timestamp_from_datetime, now
13@pytest.fixture(autouse=True)
14def _(testconfig):
15 pass
18def test_b64():
19 assert crypto.b64decode(crypto.b64encode(b"hello there")) == b"hello there"
22def test_simple_crypto():
23 assert crypto.simple_decrypt("test_simple", crypto.simple_encrypt("test_simple", b"hello there")) == b"hello there"
26def test_hash_sigs():
27 sig = crypto.generate_hash_signature(b"this is the message", crypto.get_secret("test_hash"))
28 crypto.verify_hash_signature(b"this is the message", crypto.get_secret("test_hash"), sig)
31def test_asym_crypto():
32 skey, pkey = crypto.generate_asym_keypair()
33 encrypted = crypto.asym_encrypt(pkey, b"a very secret message")
34 assert crypto.asym_decrypt(skey, encrypted) == b"a very secret message"
37def test_stable_secure_uniform():
38 # make sure it didn't change
39 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed0") == 0.17992286217826525
40 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed1") == 0.725282807072193
41 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed2") == 0.9063440288190295
42 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed3") == 0.6327659823819931
43 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed4") == 0.927720188949493
44 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed5") == 0.055950106064694194
45 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed6") == 0.5282629474672513
46 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed7") == 0.8330914059728719
47 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed8") == 0.8089643245604919
48 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed9") == 0.4034213734044777
50 # make sure it's rand unif
51 for _ in range(1000):
52 u = crypto.stable_secure_uniform(key=b"test", seed=nacl.utils.random(32))
53 assert u > 0 and u < 1
54 print(u)
56 # make sure it's stable
57 u1 = crypto.stable_secure_uniform(key=b"test", seed=b"seed1")
58 u2 = crypto.stable_secure_uniform(key=b"test", seed=b"seed1")
59 u3 = crypto.stable_secure_uniform(key=b"test", seed=b"seed1")
60 assert u1 == u2 and u2 == u3
62 # make sure it's diff
63 u4 = crypto.stable_secure_uniform(key=b"test", seed=b"seed2")
64 u5 = crypto.stable_secure_uniform(key=b"test", seed=b"seed3")
65 assert u4 != u5
67 u6 = crypto.stable_secure_uniform(key=b"test1", seed=b"seed")
68 u7 = crypto.stable_secure_uniform(key=b"test2", seed=b"seed")
69 assert u6 != u7
72def test_encrypt_decrypt_proto_roundtrip():
73 original = internal_pb2.VerificationReferencePayload(
74 verification_attempt_token="test-token-123",
75 user_id=42,
76 )
77 encrypted = crypto.encrypt_proto("test_key", original)
79 # Should be a non-empty base64 string
80 assert encrypted
81 assert isinstance(encrypted, str)
83 # Should decrypt back to the same values
84 decrypted = crypto.decrypt_proto("test_key", encrypted, internal_pb2.VerificationReferencePayload)
85 assert decrypted.verification_attempt_token == original.verification_attempt_token
86 assert decrypted.user_id == original.user_id
89def test_encrypt_decrypt_proto_with_different_fields():
90 original = internal_pb2.SofaPayload(
91 created=Timestamp_from_datetime(now()),
92 )
93 encrypted = crypto.encrypt_proto("another_key", original)
94 decrypted = crypto.decrypt_proto("another_key", encrypted, internal_pb2.SofaPayload)
96 assert decrypted.created.seconds == original.created.seconds
99def test_decrypt_proto_wrong_key():
100 original = internal_pb2.VerificationReferencePayload(
101 verification_attempt_token="test-token",
102 user_id=1,
103 )
104 encrypted = crypto.encrypt_proto("correct_key", original)
106 # Decrypting with wrong key should fail with CryptoError
107 with pytest.raises(CryptoError):
108 crypto.decrypt_proto("wrong_key", encrypted, internal_pb2.VerificationReferencePayload)
111def test_decrypt_proto_invalid_data():
112 # Invalid data should raise NaCl TypeError (nonce not long enough)
113 with pytest.raises(NaClTypeError):
114 crypto.decrypt_proto("any_key", "not-valid-base64!!!", internal_pb2.SofaPayload)
117def test_decrypt_proto_invalid_encrypted_data():
118 # Valid base64 but not valid encrypted data
119 with pytest.raises(CryptoError):
120 crypto.decrypt_proto("any_key", crypto.b64encode(b"invalid data"), internal_pb2.SofaPayload)
123def test_encrypt_proto_different_keys_different_output():
124 original = internal_pb2.VerificationReferencePayload(
125 verification_attempt_token="test-token",
126 user_id=1,
127 )
128 encrypted1 = crypto.encrypt_proto("key1", original)
129 encrypted2 = crypto.encrypt_proto("key2", original)
131 # Different keys should produce different encrypted values
132 assert encrypted1 != encrypted2
135def test_create_sofa_id():
136 sofa_id = crypto.create_sofa_id()
137 assert len(sofa_id) == 18
138 assert isinstance(sofa_id, bytes)
140 sofa_id2 = crypto.create_sofa_id()
141 assert sofa_id != sofa_id2
144def test_sofa_payload_roundtrip():
145 sofa_id = crypto.create_sofa_id()
146 original = internal_pb2.SofaPayload(created=Timestamp_from_datetime(now()))
147 signed = crypto.encode_sofa(sofa_id, original)
149 assert signed
150 assert isinstance(signed, str)
152 returned_sofa_id, verified = crypto.decode_sofa(signed)
153 assert returned_sofa_id == sofa_id
154 assert verified.created.seconds == original.created.seconds
157def test_sofa_payload_invalid_data():
158 with pytest.raises(binascii.Error):
159 crypto.decode_sofa("invalid-base64")
162def test_sofa_payload_too_short():
163 with pytest.raises(ValueError, match="too short"):
164 crypto.decode_sofa(crypto.b64encode(b"short"))
167def test_sofa_payload_tampered_sofa_id():
168 sofa_id = crypto.create_sofa_id()
169 original = internal_pb2.SofaPayload(created=Timestamp_from_datetime(now()))
170 signed = crypto.encode_sofa(sofa_id, original)
172 data = crypto.b64decode(signed)
173 tampered = bytes([data[0] ^ 0xFF]) + data[1:]
174 tampered_b64 = crypto.b64encode(tampered)
176 with pytest.raises(ValueError, match="Invalid signature"):
177 crypto.decode_sofa(tampered_b64)
180def test_sofa_payload_tampered_proto():
181 sofa_id = crypto.create_sofa_id()
182 original = internal_pb2.SofaPayload(created=Timestamp_from_datetime(now()))
183 signed = crypto.encode_sofa(sofa_id, original)
185 data = crypto.b64decode(signed)
186 proto_start = 18
187 proto_end = len(data) - 16
188 if proto_end > proto_start: 188 ↛ exitline 188 didn't return from function 'test_sofa_payload_tampered_proto' because the condition on line 188 was always true
189 tampered = data[:proto_start] + bytes([data[proto_start] ^ 0xFF]) + data[proto_start + 1 :]
190 tampered_b64 = crypto.b64encode(tampered)
192 with pytest.raises(ValueError, match="Invalid signature"):
193 crypto.decode_sofa(tampered_b64)
196def test_sofa_payload_same_id_same_output():
197 sofa_id = crypto.create_sofa_id()
198 original = internal_pb2.SofaPayload(created=Timestamp_from_datetime(now()))
199 signed1 = crypto.encode_sofa(sofa_id, original)
200 signed2 = crypto.encode_sofa(sofa_id, original)
202 assert signed1 == signed2
205def test_sofa_payload_different_ids_different_output():
206 original = internal_pb2.SofaPayload(created=Timestamp_from_datetime(now()))
207 signed1 = crypto.encode_sofa(crypto.create_sofa_id(), original)
208 signed2 = crypto.encode_sofa(crypto.create_sofa_id(), original)
210 assert signed1 != signed2