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

1import binascii 

2 

3import nacl.utils 

4import pytest 

5from nacl.exceptions import CryptoError 

6from nacl.exceptions import TypeError as NaClTypeError 

7 

8from couchers import crypto 

9from couchers.proto.internal import internal_pb2 

10from couchers.utils import Timestamp_from_datetime, now 

11 

12 

13@pytest.fixture(autouse=True) 

14def _(testconfig): 

15 pass 

16 

17 

18def test_b64(): 

19 assert crypto.b64decode(crypto.b64encode(b"hello there")) == b"hello there" 

20 

21 

22def test_simple_crypto(): 

23 assert crypto.simple_decrypt("test_simple", crypto.simple_encrypt("test_simple", b"hello there")) == b"hello there" 

24 

25 

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) 

29 

30 

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" 

35 

36 

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 

49 

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) 

55 

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 

61 

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 

66 

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 

70 

71 

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) 

78 

79 # Should be a non-empty base64 string 

80 assert encrypted 

81 assert isinstance(encrypted, str) 

82 

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 

87 

88 

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) 

95 

96 assert decrypted.created.seconds == original.created.seconds 

97 

98 

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) 

105 

106 # Decrypting with wrong key should fail with CryptoError 

107 with pytest.raises(CryptoError): 

108 crypto.decrypt_proto("wrong_key", encrypted, internal_pb2.VerificationReferencePayload) 

109 

110 

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) 

115 

116 

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) 

121 

122 

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) 

130 

131 # Different keys should produce different encrypted values 

132 assert encrypted1 != encrypted2 

133 

134 

135def test_create_sofa_id(): 

136 sofa_id = crypto.create_sofa_id() 

137 assert len(sofa_id) == 18 

138 assert isinstance(sofa_id, bytes) 

139 

140 sofa_id2 = crypto.create_sofa_id() 

141 assert sofa_id != sofa_id2 

142 

143 

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) 

148 

149 assert signed 

150 assert isinstance(signed, str) 

151 

152 returned_sofa_id, verified = crypto.decode_sofa(signed) 

153 assert returned_sofa_id == sofa_id 

154 assert verified.created.seconds == original.created.seconds 

155 

156 

157def test_sofa_payload_invalid_data(): 

158 with pytest.raises(binascii.Error): 

159 crypto.decode_sofa("invalid-base64") 

160 

161 

162def test_sofa_payload_too_short(): 

163 with pytest.raises(ValueError, match="too short"): 

164 crypto.decode_sofa(crypto.b64encode(b"short")) 

165 

166 

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) 

171 

172 data = crypto.b64decode(signed) 

173 tampered = bytes([data[0] ^ 0xFF]) + data[1:] 

174 tampered_b64 = crypto.b64encode(tampered) 

175 

176 with pytest.raises(ValueError, match="Invalid signature"): 

177 crypto.decode_sofa(tampered_b64) 

178 

179 

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) 

184 

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) 

191 

192 with pytest.raises(ValueError, match="Invalid signature"): 

193 crypto.decode_sofa(tampered_b64) 

194 

195 

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) 

201 

202 assert signed1 == signed2 

203 

204 

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) 

209 

210 assert signed1 != signed2