Coverage for tests/test_derivepassphrase.py: 100.000%
93 statements
« prev ^ index » next coverage.py v7.6.0, created at 2024-07-14 11:39 +0200
« prev ^ index » next coverage.py v7.6.0, created at 2024-07-14 11:39 +0200
1# SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info>
2#
3# SPDX-License-Identifier: MIT
5"""Test passphrase generation via derivepassphrase.Vault."""
7from __future__ import annotations
9import math
10from typing import Any
12import derivepassphrase
13import pytest
15Vault = derivepassphrase.Vault
17class TestVault:
19 phrase = b'She cells C shells bye the sea shoars'
20 google_phrase = rb': 4TVH#5:aZl8LueOT\{'
21 twitter_phrase = rb"[ (HN_N:lI&<ro=)3'g9"
23 @pytest.mark.parametrize(['service', 'expected'], [
24 (b'google', google_phrase),
25 ('twitter', twitter_phrase),
26 ])
27 def test_200_basic_configuration(self, service, expected):
28 assert Vault(phrase=self.phrase).generate(service) == expected
30 def test_201_phrase_dependence(self):
31 assert (
32 Vault(phrase=(self.phrase + b'X')).generate('google') ==
33 b'n+oIz6sL>K*lTEWYRO%7'
34 )
36 def test_202_reproducibility_and_bytes_service_name(self):
37 assert (
38 Vault(phrase=self.phrase).generate(b'google') ==
39 Vault(phrase=self.phrase).generate('google')
40 )
42 def test_203_reproducibility_and_bytearray_service_name(self):
43 assert (
44 Vault(phrase=self.phrase).generate(b'google') ==
45 Vault(phrase=self.phrase).generate(bytearray(b'google'))
46 )
48 def test_210_nonstandard_length(self):
49 assert (
50 Vault(phrase=self.phrase, length=4).generate('google')
51 == b'xDFu'
52 )
54 def test_211_repetition_limit(self):
55 assert (
56 Vault(phrase=b'', length=24, symbol=0, number=0,
57 repeat=1).generate('asd') ==
58 b'IVTDzACftqopUXqDHPkuCIhV'
59 )
61 def test_212_without_symbols(self):
62 assert (
63 Vault(phrase=self.phrase, symbol=0).generate('google') ==
64 b'XZ4wRe0bZCazbljCaMqR'
65 )
67 def test_213_no_numbers(self):
68 assert (
69 Vault(phrase=self.phrase, number=0).generate('google') ==
70 b'_*$TVH.%^aZl(LUeOT?>'
71 )
73 def test_214_no_lowercase_letters(self):
74 assert (
75 Vault(phrase=self.phrase, lower=0).generate('google') ==
76 b':{?)+7~@OA:L]!0E$)(+'
77 )
79 def test_215_at_least_5_digits(self):
80 assert (
81 Vault(phrase=self.phrase, length=8, number=5)
82 .generate('songkick') == b'i0908.7['
83 )
85 def test_216_lots_of_spaces(self):
86 assert (
87 Vault(phrase=self.phrase, space=12)
88 .generate('songkick') == b' c 6 Bq % 5fR '
89 )
91 def test_217_all_character_classes(self):
92 assert (
93 Vault(phrase=self.phrase, lower=2, upper=2, number=1,
94 space=3, dash=2, symbol=1)
95 .generate('google') == b': : fv_wqt>a-4w1S R'
96 )
98 def test_218_only_numbers_and_very_high_repetition_limit(self):
99 generated = Vault(phrase=b'', length=40, lower=0, upper=0, space=0,
100 dash=0, symbol=0, repeat=4).generate('abcdef')
101 forbidden_substrings = {b'0000', b'1111', b'2222', b'3333', b'4444',
102 b'5555', b'6666', b'7777', b'8888', b'9999'}
103 for substring in forbidden_substrings:
104 assert substring not in generated
106 def test_219_very_limited_character_set(self):
107 generated = Vault(phrase=b'', length=24, lower=0, upper=0,
108 space=0, symbol=0).generate('testing')
109 assert b'763252593304946694588866' == generated
111 def test_220_character_set_subtraction(self):
112 assert Vault._subtract(b'be', b'abcdef') == bytearray(b'acdf')
114 @pytest.mark.parametrize(['length', 'settings', 'entropy'], [
115 (20, {}, math.log2(math.factorial(20)) + 20 * math.log2(94)),
116 (
117 20,
118 {'upper': 0, 'number': 0, 'space': 0, 'symbol': 0},
119 math.log2(math.factorial(20)) + 20 * math.log2(26)
120 ),
121 (0, {}, float('-inf')),
122 (0, {'lower': 0, 'number': 0, 'space': 0, 'symbol': 0}, float('-inf')),
123 (1, {}, math.log2(94)),
124 (1, {'upper': 0, 'lower': 0, 'number': 0, 'symbol': 0}, 0.0),
125 ])
126 def test_221_entropy(
127 self, length: int, settings: dict[str, int], entropy: int
128 ) -> None:
129 v = Vault(length=length, **settings) # type: ignore[arg-type]
130 assert math.isclose(v._entropy(), entropy)
131 assert v._estimate_sufficient_hash_length() > 0
132 if math.isfinite(entropy) and entropy:
133 assert (
134 v._estimate_sufficient_hash_length(1.0) ==
135 math.ceil(entropy / 8)
136 )
137 assert v._estimate_sufficient_hash_length(8.0) >= entropy
139 def test_222_hash_length_estimation(self) -> None:
140 v = Vault(phrase=self.phrase)
141 v2 = Vault(phrase=self.phrase, lower=0, upper=0, number=0,
142 symbol=0, space=1, length=1)
143 assert v2._entropy() == 0.0
144 assert v2._estimate_sufficient_hash_length() > 0
146 @pytest.mark.parametrize(['service', 'expected'], [
147 (b'google', google_phrase),
148 ('twitter', twitter_phrase),
149 ])
150 def test_223_hash_length_expansion(
151 self, monkeypatch: Any, service: str | bytes, expected: bytes
152 ) -> None:
153 v = Vault(phrase=self.phrase)
154 monkeypatch.setattr(v,
155 '_estimate_sufficient_hash_length',
156 lambda *args, **kwargs: 1)
157 assert v._estimate_sufficient_hash_length() < len(self.phrase)
158 assert v.generate(service) == expected
160 @pytest.mark.parametrize(['s', 'raises'], [
161 ('ñ', True), ('Düsseldorf', True),
162 ('liberté, egalité, fraternité', True), ('ASCII', False),
163 ('Düsseldorf'.encode('UTF-8'), False),
164 (bytearray([2, 3, 5, 7, 11, 13]), False),
165 ])
166 def test_224_binary_strings(
167 self, s: str | bytes | bytearray, raises: bool
168 ) -> None:
169 binstr = derivepassphrase.Vault._get_binary_string
170 AmbiguousByteRepresentationError = (
171 derivepassphrase.AmbiguousByteRepresentationError
172 )
173 if raises:
174 with pytest.raises(AmbiguousByteRepresentationError):
175 binstr(s)
176 elif isinstance(s, str):
177 assert binstr(s) == s.encode('UTF-8')
178 assert binstr(binstr(s)) == s.encode('UTF-8')
179 else:
180 assert binstr(s) == bytes(s)
181 assert binstr(binstr(s)) == bytes(s)
183 def test_310_too_many_symbols(self):
184 with pytest.raises(ValueError,
185 match='requested passphrase length too short'):
186 Vault(phrase=self.phrase, symbol=100)
188 def test_311_no_viable_characters(self):
189 with pytest.raises(ValueError,
190 match='no allowed characters left'):
191 Vault(phrase=self.phrase, lower=0, upper=0, number=0,
192 space=0, dash=0, symbol=0)
194 def test_320_character_set_subtraction_duplicate(self):
195 with pytest.raises(ValueError, match='duplicate characters'):
196 Vault._subtract(b'abcdef', b'aabbccddeeff')
197 with pytest.raises(ValueError, match='duplicate characters'):
198 Vault._subtract(b'aabbccddeeff', b'abcdef')
200 def test_322_hash_length_estimation(self) -> None:
201 v = Vault(phrase=self.phrase)
202 with pytest.raises(ValueError,
203 match='invalid safety factor'):
204 assert v._estimate_sufficient_hash_length(-1.0)
205 with pytest.raises(TypeError,
206 match='invalid safety factor: not a float'):
207 assert v._estimate_sufficient_hash_length(None) # type: ignore