f76e05eff8a52e7287461c56995b329bc09ffb79
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)
Marco Ricci Clean up testing machinery...

Marco Ricci authored 2 weeks ago

34)             _result = runner.invoke(
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

35)                 cli.derivepassphrase_export,
36)                 ['VAULT_PATH'],
37)             )
Marco Ricci Clean up testing machinery...

Marco Ricci authored 2 weeks ago

38)         result = tests.ReadableResult.parse(_result)
39)         assert result.clean_exit(empty_stderr=True), 'expected clean exit'
40)         assert json.loads(result.output) == tests.VAULT_V03_CONFIG_DATA
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

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)         ):
Marco Ricci Clean up testing machinery...

Marco Ricci authored 2 weeks ago

49)             _result = runner.invoke(
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

50)                 cli.derivepassphrase_export,
51)                 ['-k', tests.VAULT_MASTER_KEY, '.vault'],
52)             )
Marco Ricci Clean up testing machinery...

Marco Ricci authored 2 weeks ago

53)         result = tests.ReadableResult.parse(_result)
54)         assert result.clean_exit(empty_stderr=True), 'expected clean exit'
55)         assert json.loads(result.output) == tests.VAULT_V03_CONFIG_DATA
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

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)         ):
Marco Ricci Clean up testing machinery...

Marco Ricci authored 2 weeks ago

93)             _result = runner.invoke(
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

94)                 cli.derivepassphrase_export,
95)                 ['-f', format, '-k', tests.VAULT_MASTER_KEY, 'VAULT_PATH'],
96)             )
Marco Ricci Clean up testing machinery...

Marco Ricci authored 2 weeks ago

97)         result = tests.ReadableResult.parse(_result)
98)         assert result.clean_exit(empty_stderr=True), 'expected clean exit'
99)         assert json.loads(result.output) == config_data
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

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)         ):
Marco Ricci Clean up testing machinery...

Marco Ricci authored 2 weeks ago

115)             _result = runner.invoke(
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

116)                 cli.derivepassphrase_export,
117)                 ['does-not-exist.txt'],
118)             )
Marco Ricci Clean up testing machinery...

Marco Ricci authored 2 weeks ago

119)         result = tests.ReadableResult.parse(_result)
120)         assert result.error_exit(
121)             error="Cannot parse 'does-not-exist.txt' as a valid config"
122)         ), 'expected error exit and known error message'
123)         assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

124) 
125)     def test_302_vault_config_invalid(
126)         self,
127)         monkeypatch: pytest.MonkeyPatch,
128)     ) -> None:
129)         runner = click.testing.CliRunner(mix_stderr=False)
130)         with tests.isolated_vault_exporter_config(
131)             monkeypatch=monkeypatch,
132)             runner=runner,
133)             vault_config='',
134)             vault_key=tests.VAULT_MASTER_KEY,
135)         ):
Marco Ricci Clean up testing machinery...

Marco Ricci authored 2 weeks ago

136)             _result = runner.invoke(
Marco Ricci Move exporter command-line...

Marco Ricci authored 2 weeks ago

137)                 cli.derivepassphrase_export,
138)                 ['.vault'],
139)             )
Marco Ricci Clean up testing machinery...

Marco Ricci authored 2 weeks ago

140)         result = tests.ReadableResult.parse(_result)
141)         assert result.error_exit(
142)             error="Cannot parse '.vault' as a valid config"
143)         ), 'expected error exit and known error message'
144)         assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 weeks ago

145) 
146)     def test_403_invalid_vault_config_bad_signature(
147)         self,
148)         monkeypatch: pytest.MonkeyPatch,
149)     ) -> None:
150)         runner = click.testing.CliRunner(mix_stderr=False)
151)         with tests.isolated_vault_exporter_config(
152)             monkeypatch=monkeypatch,
153)             runner=runner,
154)             vault_config=tests.VAULT_V02_CONFIG,
155)             vault_key=tests.VAULT_MASTER_KEY,
156)         ):
Marco Ricci Clean up testing machinery...

Marco Ricci authored 2 weeks ago

157)             _result = runner.invoke(
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 weeks ago

158)                 cli.derivepassphrase_export,
159)                 ['-f', 'v0.3', '.vault'],
160)             )
Marco Ricci Clean up testing machinery...

Marco Ricci authored 2 weeks ago

161)         result = tests.ReadableResult.parse(_result)
162)         assert result.error_exit(
163)             error="Cannot parse '.vault' as a valid config"
164)         ), 'expected error exit and known error message'
165)         assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 weeks ago

166) 
167)     def test_500_vault_config_invalid_internal(
168)         self,
169)         monkeypatch: pytest.MonkeyPatch,
170)     ) -> None:
171)         runner = click.testing.CliRunner(mix_stderr=False)
172)         with tests.isolated_vault_exporter_config(
173)             monkeypatch=monkeypatch,
174)             runner=runner,
175)             vault_config=tests.VAULT_V03_CONFIG,
176)             vault_key=tests.VAULT_MASTER_KEY,
177)         ):
178) 
179)             def _load_data(*_args: Any, **_kwargs: Any) -> None:
180)                 return None
181) 
182)             monkeypatch.setattr(cli, '_load_data', _load_data)
Marco Ricci Clean up testing machinery...

Marco Ricci authored 2 weeks ago

183)             _result = runner.invoke(
Marco Ricci Test exporter data loading...

Marco Ricci authored 2 weeks ago

184)                 cli.derivepassphrase_export,
185)                 ['.vault'],
186)             )
Marco Ricci Clean up testing machinery...

Marco Ricci authored 2 weeks ago

187)         result = tests.ReadableResult.parse(_result)
188)         assert result.error_exit(
189)             error='Invalid vault config: '
190)         ), 'expected error exit and known error message'
191)         assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr
Marco Ricci Add more tests of the store...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

301) 
302) 
303) class TestVaultNativeConfig:
304)     @pytest.mark.parametrize(
305)         ['iterations', 'result'],
306)         [
307)             (100, b'6ede361e81e9c061efcdd68aeb768b80'),
308)             (200, b'bcc7d01e075b9ffb69e702bf701187c1'),
309)         ],
310)     )
311)     def test_200_pbkdf2_manually(self, iterations: int, result: bytes) -> None:
Marco Ricci Fix formatting and linting...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

314)                 tests.VAULT_MASTER_KEY.encode('utf-8'), 32, iterations
315)             )
316)             == result
317)         )
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

318) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 2 weeks ago

319)     def test_201_export_vault_native_data_no_arguments(
320)         self, monkeypatch: pytest.MonkeyPatch
321)     ) -> None:
322)         runner = click.testing.CliRunner(mix_stderr=False)
323)         with tests.isolated_vault_exporter_config(
324)             monkeypatch=monkeypatch,
325)             runner=runner,
326)             vault_config=tests.VAULT_V03_CONFIG,
327)             vault_key=tests.VAULT_MASTER_KEY,
328)         ):
329)             parsed_config = vault_native.export_vault_native_data(None)
330)         assert parsed_config == tests.VAULT_V03_CONFIG_DATA
331) 
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

332)     @pytest.mark.parametrize(
333)         ['parser_class', 'config', 'result'],
334)         [
335)             pytest.param(
Marco Ricci Rename `vault_v03_and_below...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

337)                 tests.VAULT_V02_CONFIG,
338)                 tests.VAULT_V02_CONFIG_DATA,
339)                 id='0.2',
340)             ),
341)             pytest.param(
Marco Ricci Rename `vault_v03_and_below...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

343)                 tests.VAULT_V03_CONFIG,
344)                 tests.VAULT_V03_CONFIG_DATA,
345)                 id='0.3',
346)             ),
347)         ],
348)     )
349)     def test_300_result_caching(
350)         self,
351)         monkeypatch: pytest.MonkeyPatch,
Marco Ricci Rename `vault_v03_and_below...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

353)         config: str,
354)         result: dict[str, Any],
355)     ) -> None:
356)         def null_func(name: str) -> Callable[..., None]:
357)             def func(*_args: Any, **_kwargs: Any) -> None:  # pragma: no cover
358)                 msg = f'disallowed and stubbed out function {name} called'
359)                 raise AssertionError(msg)
Marco Ricci Fix formatting and linting...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

361)             return func
362) 
363)         runner = click.testing.CliRunner(mix_stderr=False)
364)         with tests.isolated_vault_exporter_config(
365)             monkeypatch=monkeypatch,
366)             runner=runner,
367)             vault_config=config,
368)         ):
Marco Ricci Fix formatting and linting...

Marco Ricci authored 2 weeks ago

369)             parser = parser_class(
370)                 base64.b64decode(config), tests.VAULT_MASTER_KEY
371)             )
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

374)             monkeypatch.setattr(
375)                 parser, '_parse_contents', null_func('_parse_contents')
376)             )
377)             monkeypatch.setattr(
378)                 parser, '_derive_keys', null_func('_derive_keys')
379)             )
380)             monkeypatch.setattr(
381)                 parser, '_check_signature', null_func('_check_signature')
382)             )
383)             monkeypatch.setattr(
384)                 parser, '_decrypt_payload', null_func('_decrypt_payload')
385)             )
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 weeks ago

389) 
390)     def test_400_no_password(self) -> None:
391)         with pytest.raises(ValueError, match='Password must not be empty'):