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

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 

13def test_b64(): 

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

15 

16 

17def test_simple_crypto(): 

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

19 

20 

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) 

24 

25 

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" 

30 

31 

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 

44 

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) 

50 

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 

56 

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 

61 

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 

65 

66 

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) 

73 

74 # Should be a non-empty base64 string 

75 assert encrypted 

76 assert isinstance(encrypted, str) 

77 

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 

82 

83 

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) 

90 

91 assert decrypted.created.seconds == original.created.seconds 

92 

93 

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) 

100 

101 # Decrypting with wrong key should fail with CryptoError 

102 with pytest.raises(CryptoError): 

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

104 

105 

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) 

110 

111 

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) 

116 

117 

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) 

125 

126 # Different keys should produce different encrypted values 

127 assert encrypted1 != encrypted2 

128 

129 

130def test_create_sofa_id(): 

131 sofa_id = crypto.create_sofa_id() 

132 assert len(sofa_id) == 18 

133 assert isinstance(sofa_id, bytes) 

134 

135 sofa_id2 = crypto.create_sofa_id() 

136 assert sofa_id != sofa_id2 

137 

138 

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) 

143 

144 assert signed 

145 assert isinstance(signed, str) 

146 

147 returned_sofa_id, verified = crypto.decode_sofa(signed) 

148 assert returned_sofa_id == sofa_id 

149 assert verified.created.seconds == original.created.seconds 

150 

151 

152def test_sofa_payload_invalid_data(): 

153 with pytest.raises(binascii.Error): 

154 crypto.decode_sofa("invalid-base64") 

155 

156 

157def test_sofa_payload_too_short(): 

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

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

160 

161 

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) 

166 

167 data = crypto.b64decode(signed) 

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

169 tampered_b64 = crypto.b64encode(tampered) 

170 

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

172 crypto.decode_sofa(tampered_b64) 

173 

174 

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) 

179 

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) 

186 

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

188 crypto.decode_sofa(tampered_b64) 

189 

190 

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) 

196 

197 assert signed1 == signed2 

198 

199 

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) 

204 

205 assert signed1 != signed2