e6cfc21fcaf6afac8f32d137c1ce37dddd657724
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

1) # SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info>
2) #
3) # SPDX-License-Identifier: MIT
4) 
5) from __future__ import annotations
6) 
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

7) import base64
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

8) import json
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 weeks ago

9) from typing import TYPE_CHECKING
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

10) 
11) import click.testing
12) import pytest
13) 
14) import tests
Marco Ricci Rename `vault_v03_and_below...

Marco Ricci authored 2 weeks ago

15) from derivepassphrase.exporter import cli, storeroom, vault_native
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

16) 
17) cryptography = pytest.importorskip('cryptography', minversion='38.0')
18) 
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 weeks ago

19) if TYPE_CHECKING:
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

20)     from collections.abc import Callable
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 weeks ago

21)     from typing import Any
22) 
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

23) 
24) class TestCLI:
25)     def test_200_path_parameter(self, monkeypatch: pytest.MonkeyPatch) -> None:
26)         runner = click.testing.CliRunner(mix_stderr=False)
27)         with tests.isolated_vault_exporter_config(
28)             monkeypatch=monkeypatch,
29)             runner=runner,
30)             vault_config=tests.VAULT_V03_CONFIG,
31)             vault_key=tests.VAULT_MASTER_KEY,
32)         ):
33)             monkeypatch.setenv('VAULT_KEY', tests.VAULT_MASTER_KEY)
34)             result = runner.invoke(
35)                 cli.derivepassphrase_export,
36)                 ['VAULT_PATH'],
37)             )
38)         assert not result.exception
39)         assert (result.exit_code, result.stderr_bytes) == (0, b'')
40)         assert json.loads(result.stdout) == tests.VAULT_V03_CONFIG_DATA
41) 
42)     def test_201_key_parameter(self, monkeypatch: pytest.MonkeyPatch) -> None:
43)         runner = click.testing.CliRunner(mix_stderr=False)
44)         with tests.isolated_vault_exporter_config(
45)             monkeypatch=monkeypatch,
46)             runner=runner,
47)             vault_config=tests.VAULT_V03_CONFIG,
48)         ):
49)             result = runner.invoke(
50)                 cli.derivepassphrase_export,
51)                 ['-k', tests.VAULT_MASTER_KEY, '.vault'],
52)             )
53)         assert not result.exception
54)         assert (result.exit_code, result.stderr_bytes) == (0, b'')
55)         assert json.loads(result.stdout) == tests.VAULT_V03_CONFIG_DATA
56) 
57)     @pytest.mark.parametrize(
58)         ['format', 'config', 'config_data'],
59)         [
60)             pytest.param(
61)                 'v0.2',
62)                 tests.VAULT_V02_CONFIG,
63)                 tests.VAULT_V02_CONFIG_DATA,
64)                 id='0.2',
65)             ),
66)             pytest.param(
67)                 'v0.3',
68)                 tests.VAULT_V03_CONFIG,
69)                 tests.VAULT_V03_CONFIG_DATA,
70)                 id='0.3',
71)             ),
72)             pytest.param(
73)                 'storeroom',
74)                 tests.VAULT_STOREROOM_CONFIG_ZIPPED,
75)                 tests.VAULT_STOREROOM_CONFIG_DATA,
76)                 id='storeroom',
77)             ),
78)         ],
79)     )
80)     def test_210_load_vault_v02_v03_storeroom(
81)         self,
82)         monkeypatch: pytest.MonkeyPatch,
83)         format: str,
84)         config: str | bytes,
85)         config_data: dict[str, Any],
86)     ) -> None:
87)         runner = click.testing.CliRunner(mix_stderr=False)
88)         with tests.isolated_vault_exporter_config(
89)             monkeypatch=monkeypatch,
90)             runner=runner,
91)             vault_config=config,
92)         ):
93)             result = runner.invoke(
94)                 cli.derivepassphrase_export,
95)                 ['-f', format, '-k', tests.VAULT_MASTER_KEY, 'VAULT_PATH'],
96)             )
97)         assert not result.exception
98)         assert (result.exit_code, result.stderr_bytes) == (0, b'')
99)         assert json.loads(result.stdout) == config_data
100) 
101)     # test_300_invalid_format is found in
102)     # tests.test_derivepassphrase_export::Test002CLI
103) 
104)     def test_301_vault_config_not_found(
105)         self,
106)         monkeypatch: pytest.MonkeyPatch,
107)     ) -> None:
108)         runner = click.testing.CliRunner(mix_stderr=False)
109)         with tests.isolated_vault_exporter_config(
110)             monkeypatch=monkeypatch,
111)             runner=runner,
112)             vault_config=tests.VAULT_V03_CONFIG,
113)             vault_key=tests.VAULT_MASTER_KEY,
114)         ):
115)             result = runner.invoke(
116)                 cli.derivepassphrase_export,
117)                 ['does-not-exist.txt'],
118)             )
119)         assert isinstance(result.exception, SystemExit)
120)         assert result.exit_code
121)         assert result.stderr_bytes
122)         assert (
123)             b"Cannot parse 'does-not-exist.txt' as a valid config"
124)             in result.stderr_bytes
125)         )
126)         assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr_bytes
127) 
128)     def test_302_vault_config_invalid(
129)         self,
130)         monkeypatch: pytest.MonkeyPatch,
131)     ) -> None:
132)         runner = click.testing.CliRunner(mix_stderr=False)
133)         with tests.isolated_vault_exporter_config(
134)             monkeypatch=monkeypatch,
135)             runner=runner,
136)             vault_config='',
137)             vault_key=tests.VAULT_MASTER_KEY,
138)         ):
139)             result = runner.invoke(
140)                 cli.derivepassphrase_export,
141)                 ['.vault'],
142)             )
143)         assert isinstance(result.exception, SystemExit)
144)         assert result.exit_code
145)         assert result.stderr_bytes
146)         assert (
147)             b"Cannot parse '.vault' as a valid config." in result.stderr_bytes
148)         )
149)         assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr_bytes
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 weeks ago

150) 
151)     def test_403_invalid_vault_config_bad_signature(
152)         self,
153)         monkeypatch: pytest.MonkeyPatch,
154)     ) -> None:
155)         runner = click.testing.CliRunner(mix_stderr=False)
156)         with tests.isolated_vault_exporter_config(
157)             monkeypatch=monkeypatch,
158)             runner=runner,
159)             vault_config=tests.VAULT_V02_CONFIG,
160)             vault_key=tests.VAULT_MASTER_KEY,
161)         ):
162)             result = runner.invoke(
163)                 cli.derivepassphrase_export,
164)                 ['-f', 'v0.3', '.vault'],
165)             )
166)         assert isinstance(result.exception, SystemExit)
167)         assert result.exit_code
168)         assert result.stderr_bytes
169)         assert (
170)             b"Cannot parse '.vault' as a valid config." in result.stderr_bytes
171)         )
172)         assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr_bytes
173) 
174)     def test_500_vault_config_invalid_internal(
175)         self,
176)         monkeypatch: pytest.MonkeyPatch,
177)     ) -> None:
178)         runner = click.testing.CliRunner(mix_stderr=False)
179)         with tests.isolated_vault_exporter_config(
180)             monkeypatch=monkeypatch,
181)             runner=runner,
182)             vault_config=tests.VAULT_V03_CONFIG,
183)             vault_key=tests.VAULT_MASTER_KEY,
184)         ):
185) 
186)             def _load_data(*_args: Any, **_kwargs: Any) -> None:
187)                 return None
188) 
189)             monkeypatch.setattr(cli, '_load_data', _load_data)
190)             result = runner.invoke(
191)                 cli.derivepassphrase_export,
192)                 ['.vault'],
193)             )
194)         assert isinstance(result.exception, SystemExit)
195)         assert result.exit_code
196)         assert result.stderr_bytes
197)         assert b'Invalid vault config: ' in result.stderr_bytes
198)         assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr_bytes
Marco Ricci Add more tests of the store...

Marco Ricci authored 2 weeks ago

199) 
200) 
201) class TestStoreroom:
202)     @pytest.mark.parametrize(
203)         ['path', 'key'],
204)         [
205)             ('.vault', tests.VAULT_MASTER_KEY),
206)             ('.vault', None),
207)             (None, tests.VAULT_MASTER_KEY),
208)             (None, None),
209)         ],
210)     )
211)     def test_200_export_data_path_and_keys_type(
212)         self,
213)         monkeypatch: pytest.MonkeyPatch,
214)         path: str | None,
215)         key: str | None,
216)     ) -> None:
217)         runner = click.testing.CliRunner(mix_stderr=False)
218)         with tests.isolated_vault_exporter_config(
219)             monkeypatch=monkeypatch,
220)             runner=runner,
221)             vault_config=tests.VAULT_STOREROOM_CONFIG_ZIPPED,
222)             vault_key=tests.VAULT_MASTER_KEY,
223)         ):
224)             assert (
225)                 storeroom.export_storeroom_data(path, key)
226)                 == tests.VAULT_STOREROOM_CONFIG_DATA
227)             )
228) 
229)     def test_400_decrypt_bucket_item_unknown_version(self) -> None:
230)         bucket_item = (
231)             b'\xff' + bytes(storeroom.ENCRYPTED_KEYPAIR_SIZE) + bytes(3)
232)         )
233)         master_keys: storeroom.MasterKeys = {
234)             'encryption_key': bytes(storeroom.KEY_SIZE),
235)             'signing_key': bytes(storeroom.KEY_SIZE),
236)             'hashing_key': bytes(storeroom.KEY_SIZE),
237)         }
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

238)         with pytest.raises(ValueError, match='Cannot handle version 255'):
Marco Ricci Add more tests of the store...

Marco Ricci authored 2 weeks ago

239)             storeroom.decrypt_bucket_item(bucket_item, master_keys)
240) 
241)     @pytest.mark.parametrize('config', ['xxx', 'null', '{"version": 255}'])
242)     def test_401_decrypt_bucket_file_bad_json_or_version(
243)         self,
244)         monkeypatch: pytest.MonkeyPatch,
245)         config: str,
246)     ) -> None:
247)         runner = click.testing.CliRunner(mix_stderr=False)
248)         master_keys: storeroom.MasterKeys = {
249)             'encryption_key': bytes(storeroom.KEY_SIZE),
250)             'signing_key': bytes(storeroom.KEY_SIZE),
251)             'hashing_key': bytes(storeroom.KEY_SIZE),
252)         }
253)         with (
254)             tests.isolated_vault_exporter_config(
255)                 monkeypatch=monkeypatch,
256)                 runner=runner,
257)                 vault_config=tests.VAULT_STOREROOM_CONFIG_ZIPPED,
258)             ),
259)         ):
260)             with open('.vault/20', 'w', encoding='UTF-8') as outfile:
261)                 print(config, file=outfile)
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

262)             with pytest.raises(ValueError, match='Invalid bucket file: '):
Marco Ricci Add more tests of the store...

Marco Ricci authored 2 weeks ago

263)                 list(storeroom.decrypt_bucket_file('.vault/20', master_keys))
264) 
265)     @pytest.mark.parametrize(
266)         ['data', 'err_msg'],
267)         [
268)             ('{"version": 255}', 'bad or unsupported keys version header'),
269)             ('{"version": 1}\nAAAA\nAAAA', 'trailing data; cannot make sense'),
270)             ('{"version": 1}\nAAAA', 'cannot handle version 0 encrypted keys'),
271)         ],
272)     )
273)     def test_402_export_storeroom_data_bad_master_keys_file(
274)         self,
275)         monkeypatch: pytest.MonkeyPatch,
276)         data: str,
277)         err_msg: str,
278)     ) -> None:
279)         runner = click.testing.CliRunner(mix_stderr=False)
280)         with (
281)             tests.isolated_vault_exporter_config(
282)                 monkeypatch=monkeypatch,
283)                 runner=runner,
284)                 vault_config=tests.VAULT_STOREROOM_CONFIG_ZIPPED,
285)                 vault_key=tests.VAULT_MASTER_KEY,
286)             ),
287)         ):
288)             with open('.vault/.keys', 'w', encoding='UTF-8') as outfile:
289)                 print(data, file=outfile)
290)             with pytest.raises(RuntimeError, match=err_msg):
291)                 storeroom.export_storeroom_data()
292) 
293)     def test_403_export_storeroom_data_bad_directory_listing(
294)         self,
295)         monkeypatch: pytest.MonkeyPatch,
296)     ) -> None:
297)         runner = click.testing.CliRunner(mix_stderr=False)
298)         with (
299)             tests.isolated_vault_exporter_config(
300)                 monkeypatch=monkeypatch,
301)                 runner=runner,
302)                 vault_config=tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED,
303)                 vault_key=tests.VAULT_MASTER_KEY,
304)             ),
305)             pytest.raises(RuntimeError, match='Object key mismatch'),
306)         ):
Marco Ricci Fix formatting and linting...

Marco Ricci authored 2 weeks ago

307)             storeroom.export_storeroom_data()
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

308) 
309) 
310) class TestVaultNativeConfig:
311)     @pytest.mark.parametrize(
312)         ['iterations', 'result'],
313)         [
314)             (100, b'6ede361e81e9c061efcdd68aeb768b80'),
315)             (200, b'bcc7d01e075b9ffb69e702bf701187c1'),
316)         ],
317)     )
318)     def test_200_pbkdf2_manually(self, iterations: int, result: bytes) -> None:
Marco Ricci Fix formatting and linting...

Marco Ricci authored 2 weeks ago

319)         assert (
Marco Ricci Rename `vault_v03_and_below...

Marco Ricci authored 2 weeks ago

320)             vault_native.VaultNativeConfigParser._pbkdf2(
Marco Ricci Fix formatting and linting...

Marco Ricci authored 2 weeks ago

321)                 tests.VAULT_MASTER_KEY.encode('utf-8'), 32, iterations
322)             )
323)             == result
324)         )
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

325) 
326)     @pytest.mark.parametrize(
327)         ['parser_class', 'config', 'result'],
328)         [
329)             pytest.param(
Marco Ricci Rename `vault_v03_and_below...

Marco Ricci authored 2 weeks ago

330)                 vault_native.VaultNativeV02ConfigParser,
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

331)                 tests.VAULT_V02_CONFIG,
332)                 tests.VAULT_V02_CONFIG_DATA,
333)                 id='0.2',
334)             ),
335)             pytest.param(
Marco Ricci Rename `vault_v03_and_below...

Marco Ricci authored 2 weeks ago

336)                 vault_native.VaultNativeV03ConfigParser,
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

337)                 tests.VAULT_V03_CONFIG,
338)                 tests.VAULT_V03_CONFIG_DATA,
339)                 id='0.3',
340)             ),
341)         ],
342)     )
343)     def test_300_result_caching(
344)         self,
345)         monkeypatch: pytest.MonkeyPatch,
Marco Ricci Rename `vault_v03_and_below...

Marco Ricci authored 2 weeks ago

346)         parser_class: type[vault_native.VaultNativeConfigParser],
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

347)         config: str,
348)         result: dict[str, Any],
349)     ) -> None:
350)         def null_func(name: str) -> Callable[..., None]:
351)             def func(*_args: Any, **_kwargs: Any) -> None:  # pragma: no cover
352)                 msg = f'disallowed and stubbed out function {name} called'
353)                 raise AssertionError(msg)
Marco Ricci Fix formatting and linting...

Marco Ricci authored 2 weeks ago

354) 
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

355)             return func
356) 
357)         runner = click.testing.CliRunner(mix_stderr=False)
358)         with tests.isolated_vault_exporter_config(
359)             monkeypatch=monkeypatch,
360)             runner=runner,
361)             vault_config=config,
362)         ):
Marco Ricci Fix formatting and linting...

Marco Ricci authored 2 weeks ago

363)             parser = parser_class(
364)                 base64.b64decode(config), tests.VAULT_MASTER_KEY
365)             )
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

366)             assert parser() == result
367)             # Now stub out all functions used to calculate the above result.
Marco Ricci Fix formatting and linting...

Marco Ricci authored 2 weeks ago

368)             monkeypatch.setattr(
369)                 parser, '_parse_contents', null_func('_parse_contents')
370)             )
371)             monkeypatch.setattr(
372)                 parser, '_derive_keys', null_func('_derive_keys')
373)             )
374)             monkeypatch.setattr(
375)                 parser, '_check_signature', null_func('_check_signature')
376)             )
377)             monkeypatch.setattr(
378)                 parser, '_decrypt_payload', null_func('_decrypt_payload')
379)             )
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

380)             assert parser() == result
Marco Ricci Rename `vault_v03_and_below...

Marco Ricci authored 2 weeks ago

381)             super_call = vault_native.VaultNativeConfigParser.__call__
Marco Ricci Fix formatting and linting...

Marco Ricci authored 2 weeks ago

382)             assert super_call(parser) == result
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

383) 
384)     def test_400_no_password(self) -> None:
385)         with pytest.raises(ValueError, match='Password must not be empty'):