Coverage for app/backend/src/tests/test_crypto.py: 99%
126 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +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
13def test_b64():
14 assert crypto.b64decode(crypto.b64encode(b"hello there")) == b"hello there"
17def test_simple_crypto():
18 assert crypto.simple_decrypt("test_simple", crypto.simple_encrypt("test_simple", b"hello there")) == b"hello there"
21def test_hash_sigs():
22 sig = crypto.generate_hash_signature(b"this is the message", crypto.get_secret("test_hash"))
23 crypto.verify_hash_signature(b"this is the message", crypto.get_secret("test_hash"), sig)
26def test_asym_crypto():
27 skey, pkey = crypto.generate_asym_keypair()
28 encrypted = crypto.asym_encrypt(pkey, b"a very secret message")
29 assert crypto.asym_decrypt(skey, encrypted) == b"a very secret message"
32def test_stable_secure_uniform():
33 # make sure it didn't change
34 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed0") == 0.17992286217826525
35 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed1") == 0.725282807072193
36 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed2") == 0.9063440288190295
37 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed3") == 0.6327659823819931
38 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed4") == 0.927720188949493
39 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed5") == 0.055950106064694194
40 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed6") == 0.5282629474672513
41 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed7") == 0.8330914059728719
42 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed8") == 0.8089643245604919
43 assert crypto.stable_secure_uniform(key=b"stable", seed=b"seed9") == 0.4034213734044777
45 # make sure it's rand unif
46 for _ in range(1000):
47 u = crypto.stable_secure_uniform(key=b"test", seed=nacl.utils.random(32))
48 assert u > 0 and u < 1
49 print(u)
51 # make sure it's stable
52 u1 = crypto.stable_secure_uniform(key=b"test", seed=b"seed1")
53 u2 = crypto.stable_secure_uniform(key=b"test", seed=b"seed1")
54 u3 = crypto.stable_secure_uniform(key=b"test", seed=b"seed1")
55 assert u1 == u2 and u2 == u3
57 # make sure it's diff
58 u4 = crypto.stable_secure_uniform(key=b"test", seed=b"seed2")
59 u5 = crypto.stable_secure_uniform(key=b"test", seed=b"seed3")
60 assert u4 != u5
62 u6 = crypto.stable_secure_uniform(key=b"test1", seed=b"seed")
63 u7 = crypto.stable_secure_uniform(key=b"test2", seed=b"seed")
64 assert u6 != u7
67def test_encrypt_decrypt_proto_roundtrip():
68 original = internal_pb2.VerificationReferencePayload(
69 verification_attempt_token="test-token-123",
70 user_id=42,
71 )
72 encrypted = crypto.encrypt_proto("test_key", original)
74 # Should be a non-empty base64 string
75 assert encrypted
76 assert isinstance(encrypted, str)
78 # Should decrypt back to the same values
79 decrypted = crypto.decrypt_proto("test_key", encrypted, internal_pb2.VerificationReferencePayload)
80 assert decrypted.verification_attempt_token == original.verification_attempt_token
81 assert decrypted.user_id == original.user_id
84def test_encrypt_decrypt_proto_with_different_fields():
85 original = internal_pb2.SofaPayload(
86 created=Timestamp_from_datetime(now()),
87 )
88 encrypted = crypto.encrypt_proto("another_key", original)
89 decrypted = crypto.decrypt_proto("another_key", encrypted, internal_pb2.SofaPayload)
91 assert decrypted.created.seconds == original.created.seconds
94def test_decrypt_proto_wrong_key():
95 original = internal_pb2.VerificationReferencePayload(
96 verification_attempt_token="test-token",
97 user_id=1,
98 )
99 encrypted = crypto.encrypt_proto("correct_key", original)
101 # Decrypting with wrong key should fail with CryptoError
102 with pytest.raises(CryptoError):
103 crypto.decrypt_proto("wrong_key", encrypted, internal_pb2.VerificationReferencePayload)
106def test_decrypt_proto_invalid_data():
107 # Invalid data should raise NaCl TypeError (nonce not long enough)
108 with pytest.raises(NaClTypeError):
109 crypto.decrypt_proto("any_key", "not-valid-base64!!!", internal_pb2.SofaPayload)
112def test_decrypt_proto_invalid_encrypted_data():
113 # Valid base64 but not valid encrypted data
114 with pytest.raises(CryptoError):
115 crypto.decrypt_proto("any_key", crypto.b64encode(b"invalid data"), internal_pb2.SofaPayload)
118def test_encrypt_proto_different_keys_different_output():
119 original = internal_pb2.VerificationReferencePayload(
120 verification_attempt_token="test-token",
121 user_id=1,
122 )
123 encrypted1 = crypto.encrypt_proto("key1", original)
124 encrypted2 = crypto.encrypt_proto("key2", original)
126 # Different keys should produce different encrypted values
127 assert encrypted1 != encrypted2
130def test_create_sofa_id():
131 sofa_id = crypto.create_sofa_id()
132 assert len(sofa_id) == 18
133 assert isinstance(sofa_id, bytes)
135 sofa_id2 = crypto.create_sofa_id()
136 assert sofa_id != sofa_id2
139def test_sofa_payload_roundtrip():
140 sofa_id = crypto.create_sofa_id()
141 original = internal_pb2.SofaPayload(created=Timestamp_from_datetime(now()))
142 signed = crypto.encode_sofa(sofa_id, original)
144 assert signed
145 assert isinstance(signed, str)
147 returned_sofa_id, verified = crypto.decode_sofa(signed)
148 assert returned_sofa_id == sofa_id
149 assert verified.created.seconds == original.created.seconds
152def test_sofa_payload_invalid_data():
153 with pytest.raises(binascii.Error):
154 crypto.decode_sofa("invalid-base64")
157def test_sofa_payload_too_short():
158 with pytest.raises(ValueError, match="too short"):
159 crypto.decode_sofa(crypto.b64encode(b"short"))
162def test_sofa_payload_tampered_sofa_id():
163 sofa_id = crypto.create_sofa_id()
164 original = internal_pb2.SofaPayload(created=Timestamp_from_datetime(now()))
165 signed = crypto.encode_sofa(sofa_id, original)
167 data = crypto.b64decode(signed)
168 tampered = bytes([data[0] ^ 0xFF]) + data[1:]
169 tampered_b64 = crypto.b64encode(tampered)
171 with pytest.raises(ValueError, match="Invalid signature"):
172 crypto.decode_sofa(tampered_b64)
175def test_sofa_payload_tampered_proto():
176 sofa_id = crypto.create_sofa_id()
177 original = internal_pb2.SofaPayload(created=Timestamp_from_datetime(now()))
178 signed = crypto.encode_sofa(sofa_id, original)
180 data = crypto.b64decode(signed)
181 proto_start = 18
182 proto_end = len(data) - 16
183 if proto_end > proto_start: 183 ↛ exitline 183 didn't return from function 'test_sofa_payload_tampered_proto' because the condition on line 183 was always true
184 tampered = data[:proto_start] + bytes([data[proto_start] ^ 0xFF]) + data[proto_start + 1 :]
185 tampered_b64 = crypto.b64encode(tampered)
187 with pytest.raises(ValueError, match="Invalid signature"):
188 crypto.decode_sofa(tampered_b64)
191def test_sofa_payload_same_id_same_output():
192 sofa_id = crypto.create_sofa_id()
193 original = internal_pb2.SofaPayload(created=Timestamp_from_datetime(now()))
194 signed1 = crypto.encode_sofa(sofa_id, original)
195 signed2 = crypto.encode_sofa(sofa_id, original)
197 assert signed1 == signed2
200def test_sofa_payload_different_ids_different_output():
201 original = internal_pb2.SofaPayload(created=Timestamp_from_datetime(now()))
202 signed1 = crypto.encode_sofa(crypto.create_sofa_id(), original)
203 signed2 = crypto.encode_sofa(crypto.create_sofa_id(), original)
205 assert signed1 != signed2