Add hypothesis-based test for service name dependence of derived passphrases
Marco Ricci

Marco Ricci commited on 2025-01-27 15:23:35
Zeige 1 geänderte Dateien mit 83 Einfügungen und 0 Löschungen.


As part of a larger quality assurance effort, explicitly test that
derived passphrases depend on the service name.

The obvious counterpart, that derived passphrases depend on the master
passphrase, it not quite so straightforward, and will be tackled in
a later commit.
... ...
@@ -39,6 +39,29 @@ class TestVault:
39 39
     <i>vault</i>(1)'s test suite.
40 40
     """
41 41
 
42
+    @hypothesis.given(
43
+        phrase=strategies.text(
44
+            strategies.characters(min_codepoint=32, max_codepoint=126),
45
+            min_size=1,
46
+            max_size=32,
47
+        ),
48
+        services=strategies.lists(
49
+            strategies.binary(min_size=1, max_size=32),
50
+            min_size=2,
51
+            max_size=2,
52
+            unique=True,
53
+        ),
54
+    )
55
+    def test_101_create_hash_service_name_dependence(
56
+        self,
57
+        phrase: str,
58
+        services: list[bytes],
59
+    ) -> None:
60
+        """The internal hash is dependent on the service name."""
61
+        assert Vault.create_hash(
62
+            phrase=phrase, service=services[0]
63
+        ) != Vault.create_hash(phrase=phrase, service=services[1])
64
+
42 65
     @pytest.mark.parametrize(
43 66
         ['service', 'expected'],
44 67
         [
... ...
@@ -96,6 +119,66 @@ class TestVault:
96 119
             phrase=phrase
97 120
         ).generate(bytearray(service.encode('utf-8')))
98 121
 
122
+    @hypothesis.given(
123
+        phrase=strategies.text(
124
+            strategies.characters(min_codepoint=32, max_codepoint=126),
125
+            min_size=1,
126
+            max_size=32,
127
+        ),
128
+        services=strategies.lists(
129
+            strategies.binary(min_size=1, max_size=32),
130
+            min_size=2,
131
+            max_size=2,
132
+            unique=True,
133
+        ),
134
+    )
135
+    def test_203a_service_name_dependence(
136
+        self,
137
+        phrase: str,
138
+        services: list[bytes],
139
+    ) -> None:
140
+        """The derived passphrase is dependent on the service name."""
141
+        assert Vault(phrase=phrase).generate(
142
+            services[0]
143
+        ) != Vault(phrase=phrase).generate(services[1])
144
+
145
+    @tests.hypothesis_settings_coverage_compatible
146
+    @hypothesis.given(
147
+        phrase=strategies.text(
148
+            strategies.characters(min_codepoint=32, max_codepoint=126),
149
+            min_size=1,
150
+            max_size=32,
151
+        ),
152
+        config=tests.vault_full_service_config(),
153
+        services=strategies.lists(
154
+            strategies.binary(min_size=1, max_size=32),
155
+            min_size=2,
156
+            max_size=2,
157
+            unique=True,
158
+        ),
159
+    )
160
+    def test_203b_service_name_dependence_with_config(
161
+        self,
162
+        phrase: str,
163
+        config: dict[str, int],
164
+        services: list[bytes],
165
+    ) -> None:
166
+        """The derived passphrase is dependent on the service name."""
167
+        try:
168
+            assert Vault(phrase=phrase, **config).generate(
169
+                services[0]
170
+            ) != Vault(phrase=phrase, **config).generate(services[1])
171
+        except ValueError as exc:
172
+            # The service configuration strategy attempts to only
173
+            # generate satisfiable configurations.  It is possible,
174
+            # though rare, that this fails, and that unsatisfiability is
175
+            # only recognized when actually deriving a passphrase.  In
176
+            # that case, reject the generated configuration.
177
+            hypothesis.assume('no allowed characters left' not in exc.args)
178
+            # Otherwise it's a genuine bug in the test case or the
179
+            # implementation, and should be raised.
180
+            raise  # pragma: no cover
181
+
99 182
     def test_210_nonstandard_length(self) -> None:
100 183
         """Deriving a passphrase adheres to imposed length limits."""
101 184
         assert (
102 185