Coverage for tests/test_derivepassphrase_vault.py: 100.000%

99 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-06-23 21:50 +0200

1# SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info> 

2# 

3# SPDX-License-Identifier: MIT 

4 

5"""Test passphrase generation via derivepassphrase.Vault.""" 

6 

7from __future__ import annotations 

8 

9import math 

10from typing import Any 

11 

12import derivepassphrase 

13import sequin 

14import pytest 

15import tests 

16 

17Vault = derivepassphrase.Vault 

18phrase = b'She cells C shells bye the sea shoars' 

19google_phrase = rb': 4TVH#5:aZl8LueOT\{' 

20twitter_phrase = rb"[ (HN_N:lI&<ro=)3'g9" 

21 

22@pytest.mark.parametrize(['service', 'expected'], [ 

23 (b'google', google_phrase), 

24 ('twitter', twitter_phrase), 

25]) 

26def test_200_basic_configuration(service, expected): 

27 assert Vault(phrase=phrase).generate(service) == expected 

28 

29def test_201_phrase_dependence(): 

30 assert ( 

31 Vault(phrase=(phrase + b'X')).generate('google') == 

32 b'n+oIz6sL>K*lTEWYRO%7' 

33 ) 

34 

35def test_202_reproducibility_and_bytes_service_name(): 

36 assert ( 

37 Vault(phrase=phrase).generate(b'google') == 

38 Vault(phrase=phrase).generate('google') 

39 ) 

40 

41def test_203_reproducibility_and_bytearray_service_name(): 

42 assert ( 

43 Vault(phrase=phrase).generate(b'google') == 

44 Vault(phrase=phrase).generate(bytearray(b'google')) 

45 ) 

46 

47def test_210_nonstandard_length(): 

48 assert Vault(phrase=phrase, length=4).generate('google') == b'xDFu' 

49 

50def test_211_repetition_limit(): 

51 assert ( 

52 Vault(phrase=b'', length=24, symbol=0, number=0, 

53 repeat=1).generate('asd') == 

54 b'IVTDzACftqopUXqDHPkuCIhV' 

55 ) 

56 

57def test_212_without_symbols(): 

58 assert ( 

59 Vault(phrase=phrase, symbol=0).generate('google') == 

60 b'XZ4wRe0bZCazbljCaMqR' 

61 ) 

62 

63def test_213_too_many_symbols(): 

64 with pytest.raises(ValueError, 

65 match='requested passphrase length too short'): 

66 Vault(phrase=phrase, symbol=100) 

67 

68def test_214_no_numbers(): 

69 assert ( 

70 Vault(phrase=phrase, number=0).generate('google') == 

71 b'_*$TVH.%^aZl(LUeOT?>' 

72 ) 

73 

74def test_214_no_lowercase_letters(): 

75 assert ( 

76 Vault(phrase=phrase, lower=0).generate('google') == 

77 b':{?)+7~@OA:L]!0E$)(+' 

78 ) 

79 

80def test_215_at_least_5_digits(): 

81 assert ( 

82 Vault(phrase=phrase, length=8, number=5).generate('songkick') == 

83 b'i0908.7[' 

84 ) 

85 

86def test_216_lots_of_spaces(): 

87 assert ( 

88 Vault(phrase=phrase, space=12).generate('songkick') == 

89 b' c 6 Bq % 5fR ' 

90 ) 

91 

92def test_217_no_viable_characters(): 

93 with pytest.raises(ValueError, 

94 match='no allowed characters left'): 

95 Vault(phrase=phrase, lower=0, upper=0, number=0, 

96 space=0, dash=0, symbol=0) 

97 

98def test_218_all_character_classes(): 

99 assert ( 

100 Vault(phrase=phrase, lower=2, upper=2, number=1, 

101 space=3, dash=2, symbol=1).generate('google') == 

102 b': : fv_wqt>a-4w1S R' 

103 ) 

104 

105def test_219_only_numbers_and_very_high_repetition_limit(): 

106 generated = Vault(phrase=b'', length=40, lower=0, upper=0, space=0, 

107 dash=0, symbol=0, repeat=4).generate('abcdef') 

108 assert b'0000' not in generated 

109 assert b'1111' not in generated 

110 assert b'2222' not in generated 

111 assert b'3333' not in generated 

112 assert b'4444' not in generated 

113 assert b'5555' not in generated 

114 assert b'6666' not in generated 

115 assert b'7777' not in generated 

116 assert b'8888' not in generated 

117 assert b'9999' not in generated 

118 

119def test_220_very_limited_character_set(): 

120 generated = Vault(phrase=b'', length=24, lower=0, upper=0, 

121 space=0, symbol=0).generate('testing') 

122 assert b'763252593304946694588866' == generated 

123 

124def test_300_character_set_subtraction(): 

125 assert Vault._subtract(b'be', b'abcdef') == bytearray(b'acdf') 

126 

127def test_301_character_set_subtraction_duplicate(): 

128 with pytest.raises(ValueError, match='duplicate characters'): 

129 Vault._subtract(b'abcdef', b'aabbccddeeff') 

130 with pytest.raises(ValueError, match='duplicate characters'): 

131 Vault._subtract(b'aabbccddeeff', b'abcdef') 

132 

133@pytest.mark.parametrize(['length', 'settings', 'entropy'], [ 

134 (20, {}, math.log2(math.factorial(20)) + 20 * math.log2(94)), 

135 ( 

136 20, 

137 {'upper': 0, 'number': 0, 'space': 0, 'symbol': 0}, 

138 math.log2(math.factorial(20)) + 20 * math.log2(26) 

139 ), 

140 (0, {}, float('-inf')), 

141 (0, {'lower': 0, 'number': 0, 'space': 0, 'symbol': 0}, float('-inf')), 

142 (1, {}, math.log2(94)), 

143 (1, {'upper': 0, 'lower': 0, 'number': 0, 'symbol': 0}, 0.0), 

144]) 

145def test_400_entropy( 

146 length: int, settings: dict[str, int], entropy: int 

147) -> None: 

148 v = Vault(length=length, **settings) # type: ignore[arg-type] 

149 assert math.isclose(v._entropy(), entropy) 

150 assert v._estimate_sufficient_hash_length() > 0 

151 if math.isfinite(entropy) and entropy: 

152 assert v._estimate_sufficient_hash_length(1.0) == math.ceil(entropy / 8) 

153 assert v._estimate_sufficient_hash_length(8.0) >= entropy 

154 

155def test_401_hash_length_estimation( 

156) -> None: 

157 v = Vault(phrase=phrase) 

158 with pytest.raises(ValueError, 

159 match='invalid safety factor'): 

160 assert v._estimate_sufficient_hash_length(-1.0) 

161 with pytest.raises(TypeError, 

162 match='invalid safety factor: not a float'): 

163 assert v._estimate_sufficient_hash_length(None) # type: ignore 

164 v2 = Vault(phrase=phrase, lower=0, upper=0, number=0, symbol=0, 

165 space=1, length=1) 

166 assert v2._entropy() == 0.0 

167 assert v2._estimate_sufficient_hash_length() > 0 

168 

169@pytest.mark.parametrize(['service', 'expected'], [ 

170 (b'google', google_phrase), 

171 ('twitter', twitter_phrase), 

172]) 

173def test_402_hash_length_expansion( 

174 monkeypatch: Any, service: str | bytes, expected: bytes 

175) -> None: 

176 v = Vault(phrase=phrase) 

177 monkeypatch.setattr(v, 

178 '_estimate_sufficient_hash_length', 

179 lambda *args, **kwargs: 1) 

180 assert v._estimate_sufficient_hash_length() < len(phrase) 

181 assert v.generate(service) == expected 

182 

183@pytest.mark.parametrize(['s', 'raises'], [ 

184 ('ñ', True), ('Düsseldorf', True), 

185 ('liberté, egalité, fraternité', True), ('ASCII', False), 

186 ('Düsseldorf'.encode('UTF-8'), False), 

187 (bytearray([2, 3, 5, 7, 11, 13]), False), 

188]) 

189def test_403_binary_strings(s: str | bytes | bytearray, raises: bool) -> None: 

190 binstr = derivepassphrase.Vault._get_binary_string 

191 if raises: 

192 with pytest.raises(derivepassphrase.AmbiguousByteRepresentationError): 

193 binstr(s) 

194 elif isinstance(s, str): 

195 assert binstr(s) == s.encode('UTF-8') 

196 assert binstr(binstr(s)) == s.encode('UTF-8') 

197 else: 

198 assert binstr(s) == bytes(s) 

199 assert binstr(binstr(s)) == bytes(s)